Compare commits

..

79 Commits

Author SHA1 Message Date
Ishika Choudhury
473deed90d chore: fix bal devnet 2 (#23141)
Co-authored-by: Soubhik Singha Mahapatra <soubhiksmp2004@gmail.com>
Co-authored-by: stevencartavia <112043913+stevencartavia@users.noreply.github.com>
Co-authored-by: Emma Jamieson-Hoare <ejamieson19@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-03-23 11:12:27 +00:00
Emma Jamieson-Hoare
61dd104871 Merge remote-tracking branch 'origin/main' into bal-devnet-2
Amp-Thread-ID: https://ampcode.com/threads/T-019d0bad-ef5a-716a-914d-62f3b017eada
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	.github/workflows/hive.yml
#	Cargo.lock
#	Cargo.toml
#	bin/reth-bench/src/bench/helpers.rs
#	bin/reth-bench/src/bench/persistence_waiter.rs
#	crates/engine/primitives/src/config.rs
#	crates/node/core/src/args/engine.rs
2026-03-20 15:07:27 +00:00
Ishika Choudhury
0632bc72c9 feat: fix devnet2(BAL) (#22988)
Co-authored-by: Soubhik Singha Mahapatra <soubhiksmp2004@gmail.com>
Co-authored-by: Emma Jamieson-Hoare <emmajam@users.noreply.github.com>
Co-authored-by: Emma Jamieson-Hoare <ejamieson19@gmail.com>
Co-authored-by: Soubhik Singha Mahapatra <160333583+Soubhik-10@users.noreply.github.com>
2026-03-13 12:40:09 +01:00
Emma Jamieson-Hoare
2fb2246579 chore: merge main and resolve Cargo.lock conflict
Amp-Thread-ID: https://ampcode.com/threads/T-019cd35b-fa36-71cc-84d0-90b13ee9dfb9
Co-authored-by: Amp <amp@ampcode.com>
2026-03-09 16:32:10 +00:00
Emma Jamieson-Hoare
4174045d47 fix lockfile 2026-03-09 16:26:07 +00:00
Emma Jamieson-Hoare
81262c0057 Merge remote-tracking branch 'origin/main' into bal-devnet-2
Amp-Thread-ID: https://ampcode.com/threads/T-019cd250-92a0-730a-9fac-7b7b326134a4
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	.github/scripts/hive/expected_failures.yaml
#	Cargo.lock
#	Cargo.toml
#	crates/engine/tree/src/tree/payload_validator.rs
#	crates/ethereum/evm/src/lib.rs
#	crates/evm/evm/src/execute.rs
#	crates/primitives-traits/src/account.rs
#	crates/revm/src/witness.rs
#	crates/rpc/rpc-eth-api/src/helpers/estimate.rs
#	crates/storage/provider/src/providers/database/provider.rs
2026-03-09 11:42:59 +00:00
Emma Jamieson-Hoare
4250314722 feat: add cli flags for bal (#22777) 2026-03-04 16:24:51 +00:00
Stefan
b07c223530 fix: defer insert_state until after block validation to prevent cache race (#22199)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:24:23 +00:00
Emma Jamieson-Hoare
49c05aed05 fix hive tests 2026-02-27 17:22:46 +00:00
Emma Jamieson-Hoare
a600f08593 fix tests 2026-02-27 16:51:12 +00:00
Emma Jamieson-Hoare
b5c1e0d08e fix book job 2026-02-27 16:13:55 +00:00
Emma Jamieson-Hoare
accf15e2e4 fix lockfile 2026-02-27 16:03:51 +00:00
Emma Jamieson-Hoare
e794626df6 chore: update lockfile 2026-02-27 15:57:46 +00:00
Emma Jamieson-Hoare
2e9d969033 Merge remote-tracking branch 'origin/main' into bal-devnet-2
Amp-Thread-ID: https://ampcode.com/threads/T-019c9fc0-77ab-76c8-ad48-2eb18aea3ba5
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	Cargo.lock
2026-02-27 15:39:56 +00:00
Emma Jamieson-Hoare
3e85ec2670 chore: fix hive tests 2026-02-27 15:23:18 +00:00
Emma Jamieson-Hoare
8c5b6c9b15 chore: fix hive failures (#22643) 2026-02-27 13:53:59 +00:00
Emma Jamieson-Hoare
a66d49c190 Merge branch 'main' into bal-devnet-2 2026-02-27 11:32:19 +00:00
Emma Jamieson-Hoare
ab2252c33d fix merge issues 2026-02-27 11:22:03 +00:00
Emma Jamieson-Hoare
8dbb015770 Merge remote-tracking branch 'origin/main' into bal-devnet-2 2026-02-27 11:07:57 +00:00
Ishika Choudhury
96be836679 chore: expected failing test for devnet 2 bal (#22453)
Co-authored-by: Soubhik Singha Mahapatra <soubhiksmp2004@gmail.com>
Co-authored-by: Soubhik Singha Mahapatra <160333583+Soubhik-10@users.noreply.github.com>
2026-02-27 10:56:20 +00:00
Emma Jamieson-Hoare
ab5f2db594 Merge branch 'main' into bal-devnet-2 2026-02-23 15:46:05 +00:00
Emma Jamieson-Hoare
6a633a42f0 fix: handle EIP-7778 gas accounting mismatch in payload builder (#22490)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-23 13:51:48 +01:00
Emma Jamieson-Hoare
08fd55d8c9 ci: use larger runner for hive reth builds
Amp-Thread-ID: https://ampcode.com/threads/T-019c7acf-3e9a-7459-8100-dae97ec43d52
Co-authored-by: Amp <amp@ampcode.com>
2026-02-20 11:59:27 +00:00
Emma Jamieson-Hoare
fab95c1f3a Merge branch 'main' into bal-devnet-2 2026-02-20 11:13:52 +00:00
Emma Jamieson-Hoare
e4191ccea8 Merge branch 'main' into bal-devnet-2 2026-02-19 10:27:42 +00:00
Ishika Choudhury
080ff004e3 chore: fixed bal devnet error (#22325)
Co-authored-by: Soubhik Singha Mahapatra <soubhiksmp2004@gmail.com>
2026-02-18 18:36:22 +01:00
Emma Jamieson-Hoare
18599f1732 Merge branch 'main' into bal-devnet-2 2026-02-18 12:47:57 +00:00
Emma Jamieson-Hoare
9fd35e2917 Revert "chore: merge main into devnet-2 branch (#22316)"
This reverts commit c1a5e20b50.
2026-02-18 12:40:23 +00:00
Emma Jamieson-Hoare
c1a5e20b50 chore: merge main into devnet-2 branch (#22316) 2026-02-18 12:39:07 +00:00
Ishika Choudhury
0ff16ea053 chore: fix failing tests(hive) for devnet 2 BAL (#22259) 2026-02-18 11:17:01 +01:00
Emma Jamieson-Hoare
3541bd7f65 fix rust issue 2026-02-17 12:52:09 +00:00
Emma Jamieson-Hoare
a3aec0c662 merge: resolve conflict with main in payload_validator.rs
Amp-Thread-ID: https://ampcode.com/threads/T-019c6b9f-1ef5-76f8-bc77-7547d5460bf8
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 12:42:29 +00:00
Emma Jamieson-Hoare
0dfdaca3f0 Merge branch 'main' into bal-devnet-2 2026-02-17 11:20:26 +00:00
Emma Jamieson-Hoare
c535a7fb5b Merge branch 'main' into bal-devnet-2 2026-02-16 12:26:52 -05:00
Soubhik Singha Mahapatra
bf6270b8a3 chore: try validation of bal after execution (#22165)
Co-authored-by: Ishika Choudhury <117741714+Rimeeeeee@users.noreply.github.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Emma Jamieson-Hoare <ejamieson19@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-16 18:25:20 +01:00
jenpaff
1e78685a6c fix: resolve clippy and build issues from revm hashmap changes
- Fix doc-markdown warnings in validation.rs (backtick identifiers)
- Fix StorageKeyMap type mismatch from revm specialized hashmaps
- Remove unused HashMap import

Amp-Thread-ID: https://ampcode.com/threads/T-019c66b1-1100-777b-98e4-42a7e5d2be57
Co-authored-by: Amp <amp@ampcode.com>
2026-02-16 15:53:25 +00:00
jenpaff
1f1e320643 fix: gate BytecodeKind import behind reth-codec feature
Fixes cargo hack --no-default-features check for reth-primitives-traits.

Amp-Thread-ID: https://ampcode.com/threads/T-019c66b1-1100-777b-98e4-42a7e5d2be57
Co-authored-by: Amp <amp@ampcode.com>
2026-02-16 14:38:44 +00:00
jenpaff
74ea20400e fix: add block_access_list field to BlockExecutionResult initializers
Required by alloy-evm #287 (EIP-7928). Set to None/default for now.

Amp-Thread-ID: https://ampcode.com/threads/T-019c66b1-1100-777b-98e4-42a7e5d2be57
Co-authored-by: Amp <amp@ampcode.com>
2026-02-16 14:32:20 +00:00
jenpaff
ffff5fbce2 Merge remote-tracking branch 'origin/main' into bal-devnet-2
Amp-Thread-ID: https://ampcode.com/threads/T-019c66b1-1100-777b-98e4-42a7e5d2be57
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	Cargo.lock
#	crates/rpc/rpc-engine-api/src/engine_api.rs
2026-02-16 13:51:39 +00:00
jenpaff
f514892b41 Merge remote-tracking branch 'origin/bal-devnet-2' into bal-devnet-2
Amp-Thread-ID: https://ampcode.com/threads/T-019c66b1-1100-777b-98e4-42a7e5d2be57
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	Cargo.lock
2026-02-16 13:50:56 +00:00
Soubhik Singha Mahapatra
d0ad4b0e18 chore: gas traces for failing tests (#21943)
Co-authored-by: Ishika Choudhury <117741714+Rimeeeeee@users.noreply.github.com>
2026-02-13 13:28:55 +01:00
jenpaff
cb6ed16485 chore: merge main into bal-devnet-2
Merge main into bal-devnet-2 to resolve conflicts. Key resolutions:
- Removed deleted optimism, stateless, and custom-node files (removed on main)
- Downgraded alloy workspace versions to 1.5.2 to match bal-devnet2 patches
- Added comprehensive alloy bal-devnet2 patches for all alloy crates
- Added ExecutionPayloadBodiesV2/BodyV2 type aliases (pending alloy support)
- Adapted reth-bench V4/V5 payload handling for alloy 1.5.2 API
- Kept BAL-specific changes (EIP-7778, EIP-7928)

Amp-Thread-ID: https://ampcode.com/threads/T-019c5311-f28a-7584-8224-29e16e5095c1
Co-authored-by: Amp <amp@ampcode.com>
2026-02-12 13:40:29 -05:00
Stefan
a88eef91f4 fix: pass slot_number in next_evm_env for block building (#21945)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-02-07 18:24:57 +01:00
Ishika Choudhury
f7e7afd51f chore: slot num fixes and error mapping (#21940)
Co-authored-by: Soubhik Singha Mahapatra <soubhiksmp2004@gmail.com>
2026-02-07 14:35:49 +01:00
Matthias Seitz
102764285b chore: update alloy-evm
Amp-Thread-ID: https://ampcode.com/threads/T-019c2ed0-8d62-7649-b718-257fd74ce3ce
Co-authored-by: Amp <amp@ampcode.com>
2026-02-05 18:21:00 +01:00
Matthias Seitz
4679c86003 bump evm 2026-02-04 19:41:37 +01:00
Stefan
7671838c61 fix: EIP-7778 gas accounting in receipts and block validation (#21821)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 19:18:59 +01:00
Soubhik Singha Mahapatra
8f4461c060 chore: update fixture for bal test (#21787)
Co-authored-by: Ishika Choudhury <117741714+Rimeeeeee@users.noreply.github.com>
2026-02-04 18:08:15 +01:00
Matthias Seitz
0119f3c612 bump lock 2026-02-04 13:41:10 +01:00
Stefan
094aaef5a1 fix: add engine_forkchoiceUpdatedV4 to capabilities list (#21799)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 13:29:24 +01:00
Matthias Seitz
32d03ff4d7 chore: update revm to 6aa06829d2caa2aa38606ed22b83354a7a7ff98e
Amp-Thread-ID: https://ampcode.com/threads/T-019c2389-3d07-76c9-a09f-e21b588a135c
Co-authored-by: Amp <amp@ampcode.com>
2026-02-03 13:48:25 +01:00
Matthias Seitz
3368ce6485 chore: update alloy-evm to 394f0ecf (EIP-7778 is_amsterdam fix)
Amp-Thread-ID: https://ampcode.com/threads/T-019c1e41-4c08-74bd-8901-5037d92e6fac
Co-authored-by: Amp <amp@ampcode.com>
2026-02-02 13:18:21 +01:00
Jennifer
9bc2388871 Fix bal fcu validation (#21679)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-01 20:49:50 +01:00
Matthias Seitz
1728fa97c0 bump revs 2026-01-30 15:26:49 +01:00
Ishika Choudhury
868248ec54 feat: add fcu and fixes (#21567)
Co-authored-by: Soubhik Singha Mahapatra <soubhiksmp2004@gmail.com>
2026-01-29 13:29:33 +01:00
Matthias Seitz
16ab4b8518 chore: update revm to f3b74d4ff0c6c88a09ca281323a100257fa61ebf
Amp-Thread-ID: https://ampcode.com/threads/T-019c05af-57ea-72ab-b16a-57cfaf907a9e
Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 18:40:26 +01:00
Matthias Seitz
ce74466b93 chore: update EEST fixtures to bal@v5.0.0
Amp-Thread-ID: https://ampcode.com/threads/T-019bff17-51e0-775f-840e-e67a55fc347c
Co-authored-by: Amp <amp@ampcode.com>
2026-01-27 11:56:00 +01:00
Matthias Seitz
992fc30ff5 refactor: use GotExpectedBoxed for BlockAccessListHashMismatch
Amp-Thread-ID: https://ampcode.com/threads/T-019bfc38-6e25-7093-8775-7764904c7e88
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 23:10:21 +01:00
Matthias Seitz
3ece6b6047 refactor: use with_bal_builder_if instead of conditional
Amp-Thread-ID: https://ampcode.com/threads/T-019bfc38-6e25-7093-8775-7764904c7e88
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 22:53:36 +01:00
Matthias Seitz
dec9f93ad1 fix: use block_hashes.lowest() to get lowest block number
Amp-Thread-ID: https://ampcode.com/threads/T-019bfc38-6e25-7093-8775-7764904c7e88
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 22:51:48 +01:00
Matthias Seitz
03484f76ec chore: update revm to 300efbf3e391e1796f5210cd4506508e385a55d2
Amp-Thread-ID: https://ampcode.com/threads/T-019bfc38-6e25-7093-8775-7764904c7e88
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 22:50:08 +01:00
Matthias Seitz
b870f04509 chore: update alloy to fix ExecutionPayload V4 deserializer
Amp-Thread-ID: https://ampcode.com/threads/T-019bfbfc-d6f6-73ec-b044-919aa35326fc
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 21:28:18 +01:00
Matthias Seitz
5277e59cc4 feat: validate BAL hash after block execution
- Add BlockAccessListHashMismatch variant to ConsensusError
- After execution, compute hash of built BAL and compare with expected BAL hash
- Return consensus error if hashes don't match

Amp-Thread-ID: https://ampcode.com/threads/T-019bfb9c-5974-732a-8101-32f6711e7d31
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 19:57:30 +01:00
Matthias Seitz
6271c2702f feat: add BAL support to engine tree payload validator
- Enable with_bal_builder() in execute_block when payload contains block access list
- Decode BlockAccessList from payload bytes in BlockOrPayload::block_access_list()
- Bump BAL index after pre-execution changes and after each transaction
- Add ExecutionPayload trait bound for block_access_list() method

Amp-Thread-ID: https://ampcode.com/threads/T-019bfb9c-5974-732a-8101-32f6711e7d31
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 19:52:17 +01:00
Matthias Seitz
ce15ab9f55 chore: update hive build_simulators.sh for BAL devnet
- Use bal@v4.0.0 fixtures for ethereum/eels simulator
- Add branch=eips/amsterdam/eip-7928 buildarg

Amp-Thread-ID: https://ampcode.com/threads/T-019bfb5b-9488-7388-95a2-5b93e1b3e9eb
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 19:40:55 +01:00
Matthias Seitz
0b1ec2dc89 docs: add missing docs for Amsterdam engine API endpoints
- Add doc comments for new_payload_v5 and get_payload_v6 methods
- Include links to the Amsterdam spec

Amp-Thread-ID: https://ampcode.com/threads/T-019bfb5b-9488-7388-95a2-5b93e1b3e9eb
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 19:34:59 +01:00
Matthias Seitz
5862c72880 chore: add block_access_list_hash and slot_number to HeaderExt
- Update HeaderExt to include new Amsterdam fields
- Fix EthBuiltPayload doctest to pass 5th argument
- Update Compact impl for AlloyHeader to handle new fields

Amp-Thread-ID: https://ampcode.com/threads/T-019bfb5b-9488-7388-95a2-5b93e1b3e9eb
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 19:23:11 +01:00
Matthias Seitz
8a7655ca5d chore: docs 2026-01-26 19:06:13 +01:00
Matthias Seitz
cd20adc1d4 chore: alloc 2026-01-26 19:05:13 +01:00
Matthias Seitz
f0fe45d6bf chore: fix compilation issues after BAL revert
- Remove block_access_list from BlockExecutionResult (handled in reth)
- Add block_access_list_hash and slot_number to Header initializations
- Add slot_number to PayloadAttributes initializations
- Fix clippy doc markdown warnings
- Remove unused alloy-rlp dependency from reth-evm-ethereum

Amp-Thread-ID: https://ampcode.com/threads/T-019bfb5b-9488-7388-95a2-5b93e1b3e9eb
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 18:40:46 +01:00
Matthias Seitz
00422207f4 feat: add slot_number support and enable BAL builder for Amsterdam
- Forward slot_number from PayloadAttributes to EthPayloadBuilderAttributes
- Enable BAL builder in State when Amsterdam is active
- Pin alloy-evm to rev 3df0a06 (before block_access_list in BlockExecutionResult)
- Set block_access_list_hash to None temporarily until BAL extraction is implemented

Amp-Thread-ID: https://ampcode.com/threads/T-019bfb3d-e550-767e-9df1-f6987376dbc1
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 18:05:00 +01:00
Matthias Seitz
28a94829e9 feat(consensus): add Amsterdam header field validation
- Add BlockAccessListHashMissing, BlockAccessListHashUnexpected,
  SlotNumberMissing, SlotNumberUnexpected to ConsensusError
- Add validate_amsterdam_header_fields() to consensus-common
- Clean up amsterdam.rs in payload-validator

Amp-Thread-ID: https://ampcode.com/threads/T-019bfb00-a1a7-7601-9dd6-d26171b03370
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 17:37:02 +01:00
Matthias Seitz
e081249f65 fix(engine-api): restore is_critical_method and add V5/V6 capabilities
Amp-Thread-ID: https://ampcode.com/threads/T-019bfb00-a1a7-7601-9dd6-d26171b03370
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 17:10:27 +01:00
Matthias Seitz
99fedf01f8 feat(chainspec): add Amsterdam hardfork support
- Add EMPTY_BLOCK_ACCESS_LIST_HASH constant
- Set block_access_list_hash in genesis header when Amsterdam is active
- Add amsterdam_time to create_chain_config
- Add Amsterdam to time_hardfork_opts in From<Genesis> for ChainSpec
- Add amsterdam_activated() and with_amsterdam_at() builder methods

Amp-Thread-ID: https://ampcode.com/threads/T-019bfb00-a1a7-7601-9dd6-d26171b03370
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 17:07:55 +01:00
Matthias Seitz
179e1bfc34 feat: integrate BAL devnet2 changes
- Add dependency patches for revm staging, alloy/op-alloy bal-devnet2 branches
- Cherry-pick engine API changes from PR 21203 (capabilities, engine_api, metrics, payload primitives)
- Add block_access_list_hash and slot_number fields to Header initializations
- Add slot_number field to PayloadAttributes initializations
- Update EthBuiltPayload::new() signature with block_access_list parameter
- Handle ExecutionPayload::V4 and EngineApiMessageVersion::V6 variants
- Add amsterdam payload validator

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Amp-Thread-ID: https://ampcode.com/threads/T-019bfb00-a1a7-7601-9dd6-d26171b03370
Co-authored-by: Amp <amp@ampcode.com>
2026-01-26 17:01:51 +01:00
Matthias Seitz
3adb5b9e58 Merge remote-tracking branch 'origin/staging' into bal-devnet-2 2026-01-26 16:42:55 +01:00
rakita
57d7c98f66 chore: merge main and update alloy-evm staging patch 2026-01-26 12:39:58 +01:00
rakita
5d9a43f2d4 Merge remote-tracking branch 'origin/main' into staging 2026-01-26 12:36:44 +01:00
rakita
defd0e8e5c Bump revm to staging and fix breaking changes
- Patch revm and all sub-crates to staging commit 0dc217a9
- Patch revm-inspectors to staging commit fccc4ac5
- Patch alloy-evm to staging commit 625ccc0f
- Add slot_num field to BlockEnv initializers
- Update BlockHashCache usage (no longer has keys method)
2026-01-26 02:41:26 +01:00
369 changed files with 17550 additions and 8850 deletions

View File

@@ -1,5 +0,0 @@
---
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

@@ -1,6 +0,0 @@
---
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

@@ -1,13 +0,0 @@
---
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

@@ -1,6 +0,0 @@
---
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

@@ -1,6 +0,0 @@
---
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

@@ -1,5 +0,0 @@
---
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

@@ -1,5 +0,0 @@
---
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

@@ -1,8 +0,0 @@
---
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,trie-debug",
"--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",
# 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,11 +11,10 @@
# optional branch-sha is the PR head commit for cache key
#
# Outputs:
# 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
# baseline: <source-dir>/target/profiling/reth
# feature: <source-dir>/target/profiling/reth, 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"
@@ -23,16 +22,6 @@ 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=""
@@ -73,18 +62,18 @@ case "$MODE" in
mkdir -p "${SOURCE_DIR}/target/profiling"
CACHE_VALID=false
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
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
CACHE_VALID=true
else
echo "Cached baseline binary is stale, rebuilding..."
fi
fi
if [ "$CACHE_VALID" = false ]; then
echo "Building baseline ${NODE_BIN} (${COMMIT}) from source..."
echo "Building baseline (${COMMIT}) from source..."
cd "${SOURCE_DIR}"
FEATURES_ARG=""
WORKSPACE_ARG=""
@@ -95,8 +84,8 @@ case "$MODE" in
fi
# shellcheck disable=SC2086
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
cargo build --profile profiling $NODE_PKG $WORKSPACE_ARG $FEATURES_ARG
$MC cp "target/profiling/${NODE_BIN}" "${BUCKET}/${NODE_BIN}"
cargo build --profile profiling --bin reth $WORKSPACE_ARG $FEATURES_ARG
$MC cp target/profiling/reth "${BUCKET}/reth"
fi
;;
@@ -105,34 +94,32 @@ case "$MODE" in
BUCKET="minio/reth-binaries/${BRANCH_SHA}${BUILD_SUFFIX}"
CACHE_VALID=false
if $MC stat "${BUCKET}/${NODE_BIN}" &>/dev/null && $MC stat "${BUCKET}/reth-bench" &>/dev/null; then
if $MC stat "${BUCKET}/reth" &>/dev/null && $MC stat "${BUCKET}/reth-bench" &>/dev/null; then
echo "Cache hit for ${BRANCH_SHA}, downloading binaries..."
mkdir -p "${SOURCE_DIR}/target/profiling"
$MC cp "${BUCKET}/${NODE_BIN}" "${SOURCE_DIR}/target/profiling/${NODE_BIN}"
$MC cp "${BUCKET}/reth" "${SOURCE_DIR}/target/profiling/reth"
$MC cp "${BUCKET}/reth-bench" /home/ubuntu/.cargo/bin/reth-bench
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
chmod +x "${SOURCE_DIR}/target/profiling/reth" /home/ubuntu/.cargo/bin/reth-bench
if verify_binary "${SOURCE_DIR}/target/profiling/reth" "${COMMIT}"; then
CACHE_VALID=true
else
echo "Cached feature binary is stale, rebuilding..."
fi
fi
if [ "$CACHE_VALID" = false ]; then
echo "Building feature ${NODE_BIN} (${COMMIT}) from source..."
echo "Building feature (${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 $NODE_PKG --features "${EXTRA_FEATURES}"
cargo build --profile profiling --workspace --bin reth --features "${EXTRA_FEATURES}"
else
# shellcheck disable=SC2086
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
cargo build --profile profiling $NODE_PKG
make profiling
fi
make install-reth-bench
$MC cp "target/profiling/${NODE_BIN}" "${BUCKET}/${NODE_BIN}"
$MC cp target/profiling/reth "${BUCKET}/reth"
$MC cp "$(which reth-bench)" "${BUCKET}/reth-bench"
fi
;;

View File

@@ -18,11 +18,7 @@ set -euo pipefail
LABEL="$1"
BINARY="$2"
OUTPUT_DIR="$3"
DATADIR_NAME="datadir"
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
DATADIR_NAME="datadir-big-blocks"
fi
DATADIR="$SCHELK_MOUNT/$DATADIR_NAME"
DATADIR="$SCHELK_MOUNT/datadir"
mkdir -p "$OUTPUT_DIR"
LOG="${OUTPUT_DIR}/node.log"
@@ -132,6 +128,12 @@ 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
@@ -242,7 +244,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 --wait-for-persistence)
EXTRA_BENCH_ARGS+=(--reth-new-payload)
fi
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
EXTRA_BENCH_ARGS+=(--wait-time "$BENCH_WAIT_TIME")
@@ -250,7 +252,7 @@ fi
if [ "$BIG_BLOCKS" = "true" ]; then
# Big blocks mode: replay pre-generated payloads
BIG_BLOCKS_DIR="${BENCH_BIG_BLOCKS_DIR:-${BENCH_WORK_DIR}/big-blocks}"
BIG_BLOCKS_DIR="${BENCH_WORK_DIR}/big-blocks"
# Start tracy-capture so profile only covers the benchmark
if [ "${BENCH_TRACY:-off}" != "off" ]; then
@@ -260,18 +262,9 @@ 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 \
"${BB_BENCH_ARGS[@]}" \
"${EXTRA_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,18 +22,9 @@ set -euo pipefail
MC="mc"
BUCKET="minio/reth-snapshots"
# 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}"
MANIFEST_PATH="reth-1-minimal-stable/manifest.json"
DATADIR="$SCHELK_MOUNT/datadir"
HASH_FILE="$HOME/.reth-bench-snapshot-hash"
# Fetch manifest and compute content hash for reliable freshness check
MANIFEST_CONTENT=$($MC cat "${BUCKET}/${MANIFEST_PATH}" 2>/dev/null) || {
@@ -69,7 +60,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/${SNAPSHOT_NAME}"
BASE_URL="${MINIO_ENDPOINT}/reth-snapshots/reth-1-minimal-stable"
# Rewrite manifest's base_url with the runner-reachable endpoint
MANIFEST_TMP=$(mktemp --suffix=.json)

View File

@@ -1,130 +1,31 @@
#!/usr/bin/env bash
#
# Resolves baseline and feature refs for scheduled benchmark runs.
# Resolves baseline and feature refs for nightly regression benchmark runs.
#
# 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.
# 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.
#
# Usage: bench-scheduled-refs.sh <force> <mode>
# force — "true" to run even if no new commit (bypass skip logic)
# mode — "nightly" or "hourly"
# Usage: bench-nightly-refs.sh [--force]
#
# Outputs (via GITHUB_OUTPUT):
# baseline-ref — commit SHA for baseline
# 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)
# 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
#
# Reads:
# .nightly-state/last-feature-ref (nightly, from GH Actions cache)
# .hourly-state/last-feature-ref (hourly, from GH Actions cache)
# .nightly-state/last-feature-ref (from GH Actions cache, may not exist)
#
# Requires: gh (GitHub CLI), jq, date, git (hourly mode)
# Requires: gh (GitHub CLI), jq, date
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,6 +20,5 @@
"SuperFluffy": "U095BKHB2Q4",
"kamsz": "U0A2563UBRD",
"zerosnacks": "U09FARPMN74",
"samczsun": "U096R14E4H3",
"laibe": "U09FARE0B9Q"
"samczsun": "U096R14E4H3"
}

View File

@@ -2,6 +2,9 @@
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,7 +22,6 @@ exclude_crates=(
reth-downloaders
reth-e2e-test-utils
reth-engine-service
reth-execution-cache
reth-engine-tree
reth-engine-util
reth-eth-wire
@@ -56,7 +55,6 @@ 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

@@ -3,11 +3,11 @@
#
# We'll use cargo-chef to speed up the build
#
FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
FROM lukemathwalker/cargo-chef:latest-rust-1.94.0-trixie AS chef
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y libclang-dev pkg-config
RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config
#
# We prepare the build plan

View File

@@ -9,15 +9,16 @@ go build .
./hive -client reth # first builds and caches the client
# Run each hive command in the background for each simulator and wait
# 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/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.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/bal@v5.1.0/fixtures_bal.tar.gz \
--sim.buildarg branch=err-map-3 \
--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.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/bal@v5.1.0/fixtures_bal.tar.gz \
--sim.buildarg branch=err-map-3 \
--sim.timelimit 1s || true &
./hive -client reth --sim "ethereum/engine" -sim.timelimit 1s || true &
./hive -client reth --sim "devp2p" -sim.timelimit 1s || true &

View File

@@ -1,6 +1,9 @@
# 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)
@@ -13,15 +16,19 @@ rpc-compat:
# syncing mode, the test expects syncing to be false on start
- eth_syncing/check-syncing (reth)
engine-withdrawals: [ ]
engine-withdrawals: []
engine-api: [ ]
engine-api: []
engine-cancun: [ ]
engine-cancun:
- Invalid PayloadAttributes, Missing BeaconRoot, Syncing=True (Cancun) (reth)
# the test fails with older versions of the code for which it passed before, probably related to changes
# in hive or its dependencies
- Blob Transaction Ordering, Multiple Clients (Cancun) (reth)
sync: [ ]
sync: []
engine-auth: [ ]
engine-auth: []
# EIP-7610 related tests (Revert creation in case of non-empty storage):
#
@@ -39,6 +46,10 @@ engine-auth: [ ]
#
# System contract tests (already fixed and deployed):
#
# tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout and test_invalid_log_length
# System contract is already fixed and deployed; tests cover scenarios where contract is
# malformed which can't happen retroactively. No point in adding checks.
#
# tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment
# tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment
# Post-fork system contract deployment tests. Should fix for spec compliance but not realistic
@@ -47,8 +58,44 @@ eels/consume-engine:
- tests/prague/eip7702_set_code_tx/test_set_code_txs.py::test_set_code_to_non_empty_storage[fork_Prague-blockchain_test_engine-zero_nonce]-reth
- tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-nonzero_balance]-reth
- tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-zero_balance]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_index_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
- tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-nonzero_balance]-reth
- tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-zero_balance]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Prague-blockchain_test_engine-slice_bytes_False]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Prague-blockchain_test_engine-slice_bytes_True]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_index_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Osaka-blockchain_test_engine-slice_bytes_False]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Osaka-blockchain_test_engine-slice_bytes_True]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_index_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Amsterdam-blockchain_test_engine-slice_bytes_True]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Amsterdam-blockchain_test_engine-slice_bytes_False]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Amsterdam-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Osaka-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Paris-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
@@ -99,6 +146,20 @@ eels/consume-engine:
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Shanghai-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE2-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Amsterdam-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE2-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_engine_from_state_test-opcode_CREATE-non-empty-balance-revert-initcode]-reth
# Blob limit tests:
#
@@ -193,3 +254,17 @@ eels/consume-rlp:
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_1-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_2-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Shanghai-tx_type_0-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_2-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Amsterdam-blockchain_test_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_1-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Amsterdam-tx_type_0-blockchain_test_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE2-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_opcode[fork_Amsterdam-blockchain_test_from_state_test-opcode_CREATE2-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Amsterdam-blockchain_test_from_state_test-empty-initcode]-reth

View File

@@ -1,25 +1,19 @@
# Scheduled regression benchmarks (nightly + hourly).
# Nightly regression benchmark.
#
# 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.
# 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.
#
# 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:
# 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 * * * *"
- cron: "30 5 * * *" # 06:30 UTC daily
workflow_dispatch:
inputs:
force:
description: "Force run even if no new commit (bypass skip logic)"
description: "Force run even if no new nightly (bypass skip logic)"
required: false
default: false
type: boolean
@@ -28,14 +22,6 @@ 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
@@ -49,105 +35,44 @@ permissions:
jobs:
# ---------------------------------------------------------------------------
# Job 1: Resolve refs, check staleness, manage state
# Job 1: Resolve nightly 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: 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
- name: Restore nightly state
id: state-cache
uses: actions/cache/restore@v4
with:
path: .${{ steps.mode.outputs.mode == 'hourly' && 'hourly' || 'nightly' }}-state
key: bench-${{ steps.mode.outputs.mode }}-state-dummy
path: .nightly-state
key: bench-scheduled-state-dummy
restore-keys: |
bench-${{ steps.mode.outputs.mode }}-state-
bench-scheduled-state-
- name: Resolve refs
- name: Resolve nightly refs
id: refs
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_RUN_ID: ${{ github.run_id }}
run: |
FORCE="${{ inputs.force || 'false' }}"
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 }),
});
.github/scripts/bench-scheduled-refs.sh "$FORCE"
- name: Alert on stale nightly
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true'
if: steps.refs.outputs.is-stale == 'true'
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
@@ -221,7 +146,7 @@ jobs:
}
- name: Fail on stale nightly
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true'
if: steps.refs.outputs.is-stale == 'true'
run: |
echo "::error::Nightly build is stale (>24h old). Aborting."
exit 1
@@ -242,8 +167,7 @@ jobs:
SCHELK_MOUNT: /reth-bench
BENCH_WORK_DIR: ${{ github.workspace }}/bench-work
BENCH_PR: ""
BENCH_MODE: ${{ needs.resolve-refs.outputs.mode }}
BENCH_ACTOR: "${{ needs.resolve-refs.outputs.mode }}-regression"
BENCH_ACTOR: "nightly-regression"
BENCH_BLOCKS: "2000"
BENCH_WARMUP_BLOCKS: "500"
BENCH_SAMPLY: "false"
@@ -349,8 +273,8 @@ jobs:
run: |
BASELINE_SHORT=$(echo "$BASELINE_REF" | cut -c1-8)
FEATURE_SHORT=$(echo "$FEATURE_REF" | cut -c1-8)
echo "baseline-name=${BENCH_MODE}-${BASELINE_SHORT}" >> "$GITHUB_OUTPUT"
echo "feature-name=${BENCH_MODE}-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
echo "baseline-name=nightly-${BASELINE_SHORT}" >> "$GITHUB_OUTPUT"
echo "feature-name=nightly-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
echo "baseline-ref=$BASELINE_REF" >> "$GITHUB_OUTPUT"
echo "feature-ref=$FEATURE_REF" >> "$GITHUB_OUTPUT"
@@ -466,7 +390,7 @@ jobs:
- name: Start metrics proxy
run: |
BENCH_ID="${BENCH_MODE}-${{ github.run_id }}"
BENCH_ID="nightly-${{ github.run_id }}"
BENCH_REFERENCE_EPOCH=$(date +%s)
echo "BENCH_ID=${BENCH_ID}" >> "$GITHUB_ENV"
echo "BENCH_REFERENCE_EPOCH=${BENCH_REFERENCE_EPOCH}" >> "$GITHUB_ENV"
@@ -595,7 +519,7 @@ jobs:
python3 .github/scripts/bench-reth-summary.py $SUMMARY_ARGS
- name: Generate charts
if: success() && env.BENCH_MODE != 'hourly'
if: success()
env:
BASELINE_NAME: ${{ steps.refs.outputs.baseline-name }}
FEATURE_NAME: ${{ steps.refs.outputs.feature-name }}
@@ -621,7 +545,7 @@ jobs:
- name: Push charts
id: push-charts
if: success() && env.BENCH_MODE != 'hourly'
if: success()
run: |
RUN_ID=${{ github.run_id }}
CHART_DIR="nightly/${RUN_ID}"
@@ -667,9 +591,7 @@ jobs:
const featureLink = `[\`${summary.feature.name}\`](${commitUrl}/${summary.feature.ref})`;
const diffUrl = `https://github.com/${repo}/compare/${summary.baseline.ref}...${summary.feature.ref}`;
const mode = process.env.BENCH_MODE || 'nightly';
const modeLabel = mode === 'hourly' ? 'Hourly Regression' : 'Nightly Regression';
let md = `# ${emoji} ${modeLabel}: ${label}\n\n`;
let md = `# ${emoji} Nightly Regression: ${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';
@@ -749,19 +671,9 @@ jobs:
return;
}
// Filter notifications based on mode
// Only notify on significant changes (regression OR improvement)
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;
@@ -785,9 +697,8 @@ jobs:
function cell(text) { return { type: 'raw_text', text: String(text) || ' ' }; }
const modeLabel = mode === 'hourly' ? 'Hourly Regression' : 'Nightly Regression';
const sectionText = [
`*${modeLabel}*`,
'*Nightly Regression*',
'',
`*Baseline:* ${baselineLink}`,
`*Feature:* ${featureLink}`,
@@ -803,7 +714,7 @@ jobs:
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: `${headerEmoji} ${modeLabel}: ${label}`, emoji: true },
text: { type: 'plain_text', text: `${headerEmoji} Nightly: ${label}`, emoji: true },
},
{
type: 'section',
@@ -833,7 +744,7 @@ jobs:
},
];
const text = `${modeLabel}: ${summary.baseline.name} vs ${summary.feature.name}`;
const text = `Nightly Regression: ${summary.baseline.name} vs ${summary.feature.name}`;
const resp = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
@@ -901,17 +812,14 @@ 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: ${modeLabel} Bench Failed`, emoji: true },
text: { type: 'plain_text', text: ':rotating_light: Nightly Bench Failed', emoji: true },
},
{
type: 'section',
text: { type: 'mrkdwn', text: `*${modeLabel} regression* failed while *${failedStep}*\ncc <@U09FARE0B9Q> <@U09FAL2UMLJ>` },
text: { type: 'mrkdwn', text: `*Nightly regression* failed while *${failedStep}*` },
},
{
type: 'actions',
@@ -933,7 +841,7 @@ jobs:
body: JSON.stringify({
channel,
blocks,
text: `${modeLabel} bench failed while ${failedStep}`,
text: `Nightly bench failed while ${failedStep}`,
unfurl_links: false,
}),
});
@@ -954,13 +862,11 @@ jobs:
steps:
- name: Write state file
run: |
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"
mkdir -p .nightly-state
echo "${{ needs.resolve-refs.outputs.feature-ref }}" > .nightly-state/last-feature-ref
- name: Save state
- name: Save nightly state
uses: actions/cache/save@v4
with:
path: .${{ needs.resolve-refs.outputs.mode }}-state
key: bench-${{ needs.resolve-refs.outputs.mode }}-state-${{ needs.resolve-refs.outputs.feature-ref }}
path: .nightly-state
key: bench-scheduled-state-${{ needs.resolve-refs.outputs.feature-ref }}

View File

@@ -12,15 +12,10 @@ on:
workflow_dispatch:
inputs:
blocks:
description: "Number of blocks to benchmark"
description: "Number of blocks to benchmark (or 'big' for big blocks mode)"
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
@@ -157,7 +152,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 = '${{ github.event.inputs.big_blocks }}' === 'true' ? 'true' : 'false';
bigBlocks = blocks === 'big' ? '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';
@@ -183,13 +178,14 @@ jobs:
actor = context.payload.comment.user.login;
const body = context.payload.comment.body.trim();
const intArgs = new Set(['warmup', 'cores', 'blocks']);
const intArgs = new Set(['warmup', 'cores']);
const intOrKeywordArgs = new Map([['blocks', new Set(['big'])]]);
const refArgs = new Set(['baseline', 'feature']);
const boolArgs = new Set(['samply', 'no-slack', 'big-blocks']);
const boolArgs = new Set(['samply', 'no-slack']);
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', 'big-blocks': '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', 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*/, '');
@@ -234,6 +230,15 @@ 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)`);
@@ -250,7 +255,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-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="..."]\``;
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="..."]\``;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -267,7 +272,7 @@ jobs:
samply = defaults.samply;
var noSlack = defaults['no-slack'];
cores = defaults.cores;
bigBlocks = defaults['big-blocks'];
bigBlocks = blocks === 'big' ? 'true' : 'false';
var rethNewPayload = defaults.reth_newPayload;
var abba = defaults.abba;
var otlp = defaults.otlp;
@@ -503,7 +508,6 @@ 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 || '' }}
@@ -719,39 +723,6 @@ 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: |
@@ -821,7 +792,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BENCH_REPO: ${{ github.repository }}
BENCH_RETH_BINARY: ${{ github.workspace }}/../reth-feature/target/profiling/${{ needs.reth-bench-ack.outputs.big-blocks == 'true' && 'reth-bb' || 'reth' }}
BENCH_RETH_BINARY: ${{ github.workspace }}/../reth-feature/target/profiling/reth
run: .github/scripts/bench-reth-snapshot.sh
# System tuning for reproducible benchmarks
@@ -879,31 +850,13 @@ 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"
MANIFEST="minio/reth-snapshots/reth-1-minimal-stable-big-blocks.json"
BUCKET="minio/reth-snapshots/reth-1-minimal-stable-big-blocks.tar.zst"
BIG_BLOCKS_DIR="${BENCH_WORK_DIR}/big-blocks"
rm -rf "$BIG_BLOCKS_DIR"; mkdir -p "$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 "Downloading big blocks from $BUCKET..."
$MC cat "$BUCKET" | 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"
@@ -911,8 +864,6 @@ 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: |
@@ -954,7 +905,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/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-1"
taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-1"
- name: "Run benchmark: feature (1/2)"
id: run-feature-1
@@ -965,7 +916,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/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-1"
taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-1"
- name: "Run benchmark: feature (2/2)"
if: env.BENCH_ABBA != 'false'
@@ -977,7 +928,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/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-2"
taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-2"
- name: "Run benchmark: baseline (2/2)"
if: env.BENCH_ABBA != 'false'
@@ -991,7 +942,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/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-2"
taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-2"
- name: Stop metrics proxy & generate Grafana URL
id: metrics

View File

@@ -79,4 +79,4 @@ jobs:
uses: actions/upload-artifact@v7
with:
name: ${{ inputs.artifact_name }}
path: ./artifacts
path: ./artifacts

View File

@@ -183,8 +183,6 @@ 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

@@ -5,7 +5,10 @@ name: hive
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
- cron: "0 */6 * * *"
pull_request:
branches:
- "**"
env:
CARGO_TERM_COLOR: always
@@ -15,23 +18,26 @@ concurrency:
cancel-in-progress: true
jobs:
build-reth:
uses: ./.github/workflows/docker-test.yml
prepare-reth-stable:
uses: ./.github/workflows/prepare-reth.yml
with:
hive_target: hive
artifact_name: "reth"
secrets: inherit
image_tag: ghcr.io/paradigmxyz/reth:latest
binary_name: reth
cargo_features: "asm-keccak"
artifact_name: "reth-stable"
prepare-hive:
if: github.repository == 'paradigmxyz/reth'
timeout-minutes: 45
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
runs-on:
group: Reth
steps:
- uses: actions/checkout@v6
- name: Checkout hive tests
uses: actions/checkout@v6
with:
repository: ethereum/hive
repository: Soubhik-10/hive # rm later and use ethereum/hive
ref: master
path: hivetests
- name: Get hive commit hash
@@ -77,6 +83,7 @@ jobs:
strategy:
fail-fast: false
matrix:
storage: [stable]
# ethereum/rpc to be deprecated:
# https://github.com/ethereum/hive/pull/1117
scenario:
@@ -117,26 +124,28 @@ jobs:
- sim: ethereum/rpc-compat
include:
- eth_blockNumber
- eth_call
- eth_chainId
- eth_createAccessList
- eth_estimateGas
- eth_feeHistory
- eth_getBalance
- eth_getBlockBy
- eth_getBlockTransactionCountBy
- eth_getCode
- eth_getProof
- eth_getStorage
- eth_getTransactionBy
- eth_getTransactionCount
- eth_getTransactionReceipt
- eth_sendRawTransaction
- eth_syncing
# debug_ rpc methods
- debug_
# - eth_call
# - eth_chainId
# - eth_createAccessList
# - eth_estimateGas
# - eth_feeHistory
# - eth_getBalance
# - eth_getBlockBy
# - eth_getBlockTransactionCountBy
# - eth_getCode
# - eth_getProof
# - eth_getStorage
# - eth_getTransactionBy
# - eth_getTransactionCount
# - eth_getTransactionReceipt
# - eth_sendRawTransaction
# - eth_syncing
# # debug_ rpc methods
# - debug_
# consume-engine
- sim: ethereum/eels/consume-engine
limit: .*tests/amsterdam.*
- sim: ethereum/eels/consume-engine
limit: .*tests/osaka.*
- sim: ethereum/eels/consume-engine
@@ -157,6 +166,8 @@ jobs:
limit: .*tests/paris.*
# consume-rlp
- sim: ethereum/eels/consume-rlp
limit: .*tests/amsterdam.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/osaka.*
- sim: ethereum/eels/consume-rlp
@@ -176,9 +187,9 @@ jobs:
- sim: ethereum/eels/consume-rlp
limit: .*tests/paris.*
needs:
- build-reth
- prepare-reth-stable
- prepare-hive
name: ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
name: ${{ matrix.storage }} / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
# Use larger runners for eels tests to avoid OOM runner crashes
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
permissions:
@@ -197,7 +208,7 @@ jobs:
- name: Download reth image
uses: actions/download-artifact@v8
with:
name: reth
name: reth-${{ matrix.storage }}
path: /tmp
- name: Load Docker images
@@ -211,7 +222,7 @@ jobs:
- name: Checkout hive tests
uses: actions/checkout@v6
with:
repository: ethereum/hive
repository: Soubhik-10/hive
ref: master
path: hivetests

View File

@@ -237,6 +237,31 @@ 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' }}
@@ -272,6 +297,7 @@ jobs:
- typos
- grafana
- no-test-deps
- features
- feature-propagation
- deny
timeout-minutes: 30

61
.github/workflows/prepare-reth.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Prepare Reth Image
on:
workflow_call:
inputs:
image_tag:
required: true
type: string
description: "Docker image tag to use"
binary_name:
required: false
type: string
default: "reth"
description: "Binary name to build (reth or op-reth)"
cargo_features:
required: false
type: string
default: "asm-keccak"
description: "Cargo features to enable"
cargo_package:
required: false
type: string
description: "Optional cargo package path"
artifact_name:
required: false
type: string
default: "artifacts"
description: "Name for the uploaded artifact"
jobs:
prepare-reth:
if: github.repository == 'paradigmxyz/reth'
timeout-minutes: 45
runs-on: depot-ubuntu-latest-4
steps:
- uses: actions/checkout@v6
- run: mkdir artifacts
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and export reth image
uses: docker/build-push-action@v6
with:
context: .
file: .github/scripts/hive/Dockerfile
tags: ${{ inputs.image_tag }}
outputs: type=docker,dest=./artifacts/reth_image.tar
build-args: |
CARGO_BIN=${{ inputs.binary_name }}
MANIFEST_PATH=${{ inputs.cargo_package }}
FEATURES=${{ inputs.cargo_features }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Upload reth image
id: upload
uses: actions/upload-artifact@v6
with:
name: ${{ inputs.artifact_name }}
path: ./artifacts

549
AGENTS.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
```

View File

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

549
CLAUDE.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
```

1169
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@ exclude = [".github/"]
[workspace]
members = [
"bin/reth-bb/",
"bin/reth-bench/",
"bin/reth/",
"crates/storage/rpc-provider/",
@@ -27,7 +26,6 @@ members = [
"crates/engine/invalid-block-hooks/",
"crates/engine/local",
"crates/engine/primitives/",
"crates/engine/execution-cache/",
"crates/engine/tree/",
"crates/engine/util/",
"crates/era",
@@ -78,6 +76,8 @@ 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,6 +99,8 @@ 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/",
@@ -109,6 +111,7 @@ members = [
"crates/storage/nippy-jar/",
"crates/storage/provider/",
"crates/storage/storage-api/",
"crates/storage/zstd-compressors/",
"crates/tasks/",
"crates/tokio-util/",
"crates/tracing/",
@@ -134,7 +137,6 @@ members = [
"examples/exex-subscription",
"examples/exex-test",
"examples/full-contract-state",
"examples/migrate-trie-to-packed",
"examples/manual-p2p/",
"examples/network-txpool/",
"examples/network/",
@@ -326,8 +328,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 = { version = "0.1.0", default-features = false }
reth-codecs-derive = "0.1.0"
reth-codecs = { path = "crates/storage/codecs" }
reth-codecs-derive = { path = "crates/storage/codecs/derive" }
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 }
@@ -343,7 +345,6 @@ 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" }
@@ -395,7 +396,8 @@ 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-traits = { version = "0.1.0", default-features = false }
reth-primitives = { path = "crates/primitives", default-features = false, features = ["__internal"] }
reth-primitives-traits = { path = "crates/primitives-traits", default-features = false }
reth-provider = { path = "crates/storage/provider" }
reth-prune = { path = "crates/prune/prune" }
reth-prune-types = { path = "crates/prune/types", default-features = false }
@@ -411,7 +413,6 @@ 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 }
@@ -430,7 +431,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 = { version = "0.1.0", default-features = false }
reth-zstd-compressors = { path = "crates/storage/zstd-compressors", default-features = false }
# revm
revm = { version = "36.0.0", default-features = false }
@@ -443,51 +444,55 @@ revm-database-interface = { version = "10.0.0", default-features = false }
revm-inspectors = "0.36.0"
# eth
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-dyn-abi = "1.5.0"
alloy-primitives = { version = "1.5.0", default-features = false, features = ["map-foldhash"] }
alloy-sol-types = { version = "1.5.0", default-features = false }
alloy-chains = { version = "0.2.33", default-features = false }
alloy-chains = { version = "0.2.5", 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.30.0", default-features = false }
alloy-eip7928 = { version = "0.3.3", default-features = false, features = ["rlp"] }
alloy-evm = { version = "0.29.2", 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.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 }
alloy-consensus = { version = "1.7.3", default-features = false }
alloy-contract = { version = "1.7.3", default-features = false }
alloy-eips = { version = "1.7.3", default-features = false }
alloy-genesis = { version = "1.7.3", default-features = false }
alloy-json-rpc = { version = "1.7.3", default-features = false }
alloy-network = { version = "1.7.3", default-features = false }
alloy-network-primitives = { version = "1.7.3", default-features = false }
alloy-provider = { version = "1.7.3", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "1.7.3", default-features = false }
alloy-rpc-client = { version = "1.7.3", default-features = false }
alloy-rpc-types = { version = "1.7.3", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "1.7.3", default-features = false }
alloy-rpc-types-anvil = { version = "1.7.3", default-features = false }
alloy-rpc-types-beacon = { version = "1.7.3", default-features = false }
alloy-rpc-types-debug = { version = "1.7.3", default-features = false }
alloy-rpc-types-engine = { version = "1.7.3", default-features = false }
alloy-rpc-types-eth = { version = "1.7.3", default-features = false }
alloy-rpc-types-mev = { version = "1.7.3", default-features = false }
alloy-rpc-types-trace = { version = "1.7.3", default-features = false }
alloy-rpc-types-txpool = { version = "1.7.3", default-features = false }
alloy-serde = { version = "1.7.3", default-features = false }
alloy-signer = { version = "1.7.3", default-features = false }
alloy-signer-local = { version = "1.7.3", default-features = false }
alloy-transport = { version = "1.7.3" }
alloy-transport-http = { version = "1.7.3", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "1.7.3", default-features = false }
alloy-transport-ws = { version = "1.7.3", default-features = false }
# op
op-alloy-rpc-types = { version = "0.24.0", default-features = false }
op-alloy-rpc-types-engine = { version = "0.24.0", default-features = false }
op-alloy-consensus = { version = "0.24.0", default-features = false }
alloy-op-hardforks = "0.4.4"
op-alloy-rpc-types = { version = "0.23.1", default-features = false }
op-alloy-rpc-types-engine = { version = "0.23.1", default-features = false }
op-alloy-network = { version = "0.23.1", default-features = false }
op-alloy-consensus = { version = "0.23.1", default-features = false }
op-alloy-rpc-jsonrpsee = { version = "0.23.1", default-features = false }
op-alloy-flz = { version = "0.13.1", default-features = false }
# misc
either = { version = "1.15.0", default-features = false }
@@ -526,13 +531,13 @@ quanta = "0.12"
paste = "1.0"
rand = "0.9"
rayon = "1.7"
thread-priority = "3.0.0"
rustc-hash = { version = "2.0", default-features = false }
schnellru = "0.2"
serde = { version = "1.0", default-features = false }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
serde_with = { version = "3", default-features = false, features = ["macros"] }
sha2 = { version = "0.10", default-features = false }
shellexpand = "3.0.0"
shlex = "1.3"
slotmap = "1"
smallvec = "1"
@@ -540,6 +545,7 @@ strum = { version = "0.27", default-features = false }
strum_macros = "0.27"
syn = "2.0"
thiserror = { version = "2.0.0", default-features = false }
thread-priority = "3.0.0"
tar = "0.4.44"
tracing = { version = "0.1.0", default-features = false, features = ["attributes"] }
tracing-appender = "0.2"
@@ -579,7 +585,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.13", default-features = false, features = ["rustls", "stream"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots", "stream"] }
tracing-futures = "0.2"
tower = "0.5"
tower-http = "0.6"
@@ -645,7 +651,6 @@ 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"] }
@@ -680,6 +685,7 @@ 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 }
@@ -692,7 +698,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.7"
tracing-logfmt = "=0.3.5"
tracing-samply = "0.1"
tracing-subscriber = { version = "0.3", default-features = false }
tracing-tracy = "0.11"
@@ -750,3 +756,68 @@ ipnet = "2.11"
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "9bc2dba" }
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "3020ea8" }
# alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
# alloy-op-evm = { git = "https://github.com/alloy-rs/evm", rev = "072c248" }
# =============================================================================
# BAL devnet patches (EIP-7778, EIP-7928 block access lists)
# =============================================================================
# revm staging patches
revm = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-database = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-state = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-context = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
op-revm = { git = "https://github.com/bluealloy/revm", rev = "fa5a6914398c9b178422efc1edd1d2ab33dad923" }
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", branch = "staging" }
# alloy-evm bal-devnet2 patches
alloy-evm = { git = "https://github.com/alloy-rs/evm", branch = "bal-devnet2" }
# alloy bal-devnet2 patches
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-consensus-any = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-contract = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-eips = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-genesis = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-network = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-network-primitives = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-provider = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-pubsub = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types-admin = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types-anvil = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types-beacon = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types-debug = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types-any = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types-mev = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types-trace = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-rpc-types-txpool = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-serde = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-signer = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-signer-local = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-transport = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
alloy-tx-macros = { git = "https://github.com/alloy-rs/alloy", branch = "bal-devnet2" }
# op-alloy bal-devnet2 patches
op-alloy-consensus = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
op-alloy-network = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
op-alloy-rpc-types = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }
op-alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/op-alloy", branch = "bal-devnet2" }

View File

@@ -29,7 +29,7 @@ EF_TESTS_URL := https://github.com/ethereum/tests/archive/refs/tags/$(EF_TESTS_T
EF_TESTS_DIR := ./testing/ef-tests/ethereum-tests
# The release tag of https://github.com/ethereum/execution-spec-tests to use for EEST tests
EEST_TESTS_TAG := v4.5.0
EEST_TESTS_TAG := bal@v5.0.0
EEST_TESTS_URL := https://github.com/ethereum/execution-spec-tests/releases/download/$(EEST_TESTS_TAG)/fixtures_stable.tar.gz
EEST_TESTS_DIR := ./testing/ef-tests/execution-spec-tests
@@ -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)" --profile "reproducible" --locked --target x86_64-unknown-linux-gnu
cargo build --bin reth --features "$(FEATURES) jemalloc-unprefixed" --profile "reproducible" --locked --target x86_64-unknown-linux-gnu
.PHONY: build-debug
build-debug: ## Build the reth binary into `target/debug` directory.

View File

@@ -1,100 +0,0 @@
[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"

View File

@@ -1,67 +0,0 @@
# 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.

View File

@@ -1,570 +0,0 @@
//! 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

@@ -1,291 +0,0 @@
//! 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);
}
}

View File

@@ -1,374 +0,0 @@
//! 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,13 +14,9 @@ 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
@@ -30,7 +26,6 @@ reth-rpc-api.workspace = true
reth-tracing.workspace = true
# alloy
alloy-consensus.workspace = true
alloy-eips.workspace = true
alloy-json-rpc.workspace = true
@@ -84,54 +79,39 @@ 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,7 +52,8 @@ impl InnerTransport {
url: Url,
jwt: JwtSecret,
) -> Result<(Self, Claims), AuthenticatedTransportError> {
let mut client_builder = reqwest::Client::builder();
let mut client_builder =
reqwest::Client::builder().tls_built_in_root_certs(url.scheme() == "https");
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, WaitForPersistence};
use reth_node_core::args::BenchmarkArgs;
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,
/// Controls when `reth_newPayload` waits for persistence.
pub(crate) wait_for_persistence: WaitForPersistence,
/// Whether to skip waiting for persistence (pass `wait_for_persistence: false`).
pub(crate) no_wait_for_persistence: bool,
/// Whether to skip waiting for caches (pass `wait_for_caches: false`).
pub(crate) no_wait_for_caches: bool,
}
@@ -166,9 +166,8 @@ 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,
@@ -178,7 +177,7 @@ impl BenchContext {
is_optimism,
use_reth_namespace,
rlp_blocks,
wait_for_persistence,
no_wait_for_persistence,
no_wait_for_caches,
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,7 @@ mod context;
mod generate_big_block;
pub(crate) mod helpers;
pub use generate_big_block::{
compute_payload_block_hash, BigBlockPayload, RawTransaction, RpcTransactionSource,
TransactionCollector, TransactionSource,
RawTransaction, RpcTransactionSource, TransactionCollector, TransactionSource,
};
pub(crate) mod metrics_scraper;
mod new_payload_fcu;
@@ -51,16 +50,16 @@ pub enum Subcommands {
/// --jwt-secret $(cat ~/.local/share/reth/mainnet/jwt.hex)`
SendPayload(send_payload::Command),
/// Generate a large block by merging consecutive blocks from an RPC.
/// Generate a large block by packing transactions from existing blocks.
///
/// 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.
/// This command fetches transactions from real blocks and packs them into a single
/// block using the `testing_buildBlockV1` RPC endpoint.
///
/// Example:
///
/// `reth-bench generate-big-block --rpc-url http://localhost:8545 --from-block 20000000
/// --count 10 --output-dir ./payloads`
/// `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`
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,
wait_for_persistence,
no_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,
wait_for_persistence,
no_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,
wait_for_persistence,
no_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,
wait_for_persistence,
no_wait_for_persistence,
no_wait_for_caches,
)?;

View File

@@ -3,7 +3,6 @@
use crate::{
authenticated_transport::AuthenticatedTransportConnect,
bench::{
generate_big_block::BigBlockPayload,
helpers::parse_duration,
metrics_scraper::MetricsScraper,
output::{
@@ -22,9 +21,7 @@ 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,
@@ -60,7 +57,8 @@ pub struct Command {
#[arg(long, value_name = "SKIP", default_value = "0")]
skip: usize,
/// Deprecated: gas ramp is no longer needed. This flag is accepted but ignored.
/// 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.
#[arg(long, value_name = "GAS_RAMP_DIR", hide = true)]
gas_ramp_dir: Option<PathBuf>,
@@ -83,22 +81,12 @@ pub struct Command {
#[arg(long, default_value = "false", verbatim_doc_comment)]
reth_new_payload: bool,
/// Control when `reth_newPayload` waits for in-flight persistence.
/// Skip waiting for in-flight persistence before processing.
///
/// 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>,
/// 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,
/// Skip waiting for execution cache and sparse trie locks before processing.
///
@@ -120,12 +108,10 @@ pub struct Command {
struct LoadedPayload {
/// The index (from filename).
index: u64,
/// The execution data for the block.
execution_data: ExecutionData,
/// The payload envelope.
envelope: ExecutionPayloadEnvelopeV4,
/// The block hash.
block_hash: B256,
/// Big block data containing environment switches and prior block hashes.
big_block_data: BigBlockData<ExecutionData>,
}
impl Command {
@@ -174,7 +160,8 @@ impl Command {
if self.gas_ramp_dir.is_some() {
warn!(
target: "reth-bench",
"--gas-ramp-dir is deprecated and ignored."
"--gas-ramp-dir is deprecated and ignored. Use --testing.skip-gas-limit-ramp-check \
and --testing.gas-limit on the reth node instead."
);
}
@@ -185,33 +172,22 @@ 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 execution_data = &payload.execution_data;
let envelope = &payload.envelope;
let block_hash = payload.block_hash;
let v1 = execution_data.payload.as_v1();
let execution_payload = &envelope.envelope_inner.execution_payload;
let inner_payload = &execution_payload.payload_inner.payload_inner;
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;
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;
debug!(
target: "reth-bench",
@@ -232,36 +208,34 @@ impl Command {
);
let (version, params) = if self.reth_new_payload {
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 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 wait_for_persistence = self
.wait_for_persistence
.unwrap_or(WaitForPersistence::Never)
.rpc_value(block_number);
(
None,
serde_json::to_value((
RethNewPayloadInput::ExecutionData(execution_data.clone()),
wait_for_persistence,
RethNewPayloadInput::ExecutionData(reth_data),
self.no_wait_for_persistence.then_some(false),
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_data.payload.clone(),
execution_payload.clone(),
Vec::<B256>::new(),
B256::ZERO,
requests,
envelope.execution_requests.to_vec(),
))?,
)
};
@@ -317,10 +291,6 @@ 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));
@@ -356,42 +326,28 @@ 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 — match both legacy "payload_block_*.json" and new
// "big_block_*.json" formats
// Read directory entries
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") &&
(name_str.starts_with("payload_block_") ||
name_str.starts_with("big_block_"))
e.file_name().to_string_lossy().starts_with("payload_block_")
})
.collect();
// Parse filenames to get indices and sort.
// Supports "payload_block_N.json" and "big_block_FROM_to_TO.json" naming.
// Parse filenames to get indices and sort
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();
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;
};
// 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()?;
Some((index, e.path()))
})
.collect();
@@ -409,42 +365,21 @@ 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))?;
// 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;
let block_hash =
envelope.envelope_inner.execution_payload.payload_inner.payload_inner.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, execution_data, block_hash, big_block_data });
payloads.push(LoadedPayload { index, envelope, block_hash });
}
Ok(payloads)

View File

@@ -240,6 +240,7 @@ impl Command {
ExecutionPayload::V1(p) => config.apply_to_payload_v1(p),
ExecutionPayload::V2(p) => config.apply_to_payload_v2(p),
ExecutionPayload::V3(p) => config.apply_to_payload_v3(p),
ExecutionPayload::V4(p) => config.apply_to_payload_v3(&mut p.payload_inner),
};
let skip_recalc = self.skip_hash_recalc || config.should_skip_hash_recalc();
@@ -254,6 +255,9 @@ impl Command {
ExecutionPayload::V1(p) => p.block_hash,
ExecutionPayload::V2(p) => p.payload_inner.block_hash,
ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash,
ExecutionPayload::V4(p) => {
p.payload_inner.payload_inner.payload_inner.block_hash
}
}
}
};
@@ -262,6 +266,9 @@ impl Command {
ExecutionPayload::V1(p) => p.block_hash = new_hash,
ExecutionPayload::V2(p) => p.payload_inner.block_hash = new_hash,
ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash = new_hash,
ExecutionPayload::V4(p) => {
p.payload_inner.payload_inner.payload_inner.block_hash = new_hash
}
}
}

View File

@@ -14,9 +14,6 @@
#[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,7 +12,6 @@ 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;
@@ -169,25 +168,22 @@ where
///
/// Returns `(version, versioned_params, execution_data)`.
///
/// `wait_for_persistence` controls how `wait_for_persistence` is passed to
/// `reth_newPayload` on a per-block basis.
/// 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.
pub(crate) fn block_to_new_payload(
block: AnyRpcBlock,
is_optimism: bool,
rlp: Option<Bytes>,
reth_new_payload: bool,
wait_for_persistence: WaitForPersistence,
no_wait_for_persistence: bool,
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),
wait_for_persistence,
no_wait_for_persistence.then_some(false),
no_wait_for_caches.then_some(false),
))?,
));
@@ -211,7 +207,7 @@ pub(crate) fn block_to_new_payload(
None,
serde_json::to_value((
RethNewPayloadInput::ExecutionData(execution_data),
wait_for_persistence,
no_wait_for_persistence.then_some(false),
no_wait_for_caches.then_some(false),
))?,
))
@@ -234,6 +230,20 @@ pub(crate) fn payload_to_new_payload(
let execution_data = ExecutionData { payload: payload.clone(), sidecar: sidecar.clone() };
let (version, params) = match payload {
ExecutionPayload::V4(payload) => {
let cancun = sidecar.cancun().unwrap();
let prague = sidecar.prague().unwrap();
let requests = prague.requests.requests_hash();
(
EngineApiMessageVersion::V5,
serde_json::to_value((
payload,
cancun.versioned_hashes.clone(),
cancun.parent_beacon_block_root,
requests,
))?,
)
}
ExecutionPayload::V3(payload) => {
let cancun = sidecar.cancun().unwrap();
@@ -391,9 +401,12 @@ pub(crate) async fn call_forkchoice_updated<N, P: EngineApiValidWaitExt<N>>(
forkchoice_state: ForkchoiceState,
payload_attributes: Option<PayloadAttributes>,
) -> TransportResult<ForkchoiceUpdated> {
// FCU V3 is used for both Cancun and Prague (there is no FCU V4)
// FCU V3 is used for Cancun, Prague, and Amsterdam (there is no FCU V4-V6)
match message_version {
EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 | EngineApiMessageVersion::V5 => {
EngineApiMessageVersion::V3 |
EngineApiMessageVersion::V4 |
EngineApiMessageVersion::V5 |
EngineApiMessageVersion::V6 => {
provider.fork_choice_updated_v3_wait(forkchoice_state, payload_attributes).await
}
EngineApiMessageVersion::V2 => {

View File

@@ -70,7 +70,7 @@ aquamarine.workspace = true
clap = { workspace = true, features = ["derive", "env"] }
[dev-dependencies]
alloy-node-bindings = "1.6.3"
alloy-node-bindings = "1.5.2"
alloy-provider = { workspace = true, features = ["reqwest"] }
alloy-rpc-types-eth.workspace = true
backon.workspace = true
@@ -127,7 +127,7 @@ jemalloc = [
"reth-provider/jemalloc",
]
jemalloc-prof = [
"jemalloc",
"reth-cli-util/jemalloc",
"reth-cli-util/jemalloc-prof",
"reth-ethereum-cli/jemalloc-prof",
"reth-node-metrics/jemalloc-prof",
@@ -136,6 +136,12 @@ 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,6 +22,7 @@
//! 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,12 +3,8 @@
#[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 = "malloc_conf")]
#[unsafe(export_name = "_rjem_malloc_conf")]
static MALLOC_CONF: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";
use clap::Parser;

View File

@@ -28,7 +28,7 @@ use alloy_consensus::{
};
use alloy_eips::{
eip1559::INITIAL_BASE_FEE, eip7685::EMPTY_REQUESTS_HASH, eip7840::BlobParams,
eip7892::BlobScheduleBlobParams,
eip7892::BlobScheduleBlobParams, eip7928::EMPTY_BLOCK_ACCESS_LIST_HASH,
};
use alloy_genesis::{ChainConfig, Genesis};
use alloy_primitives::{address, b256, Address, BlockNumber, B256, U256};
@@ -76,6 +76,18 @@ pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Hea
.active_at_timestamp(genesis.timestamp)
.then_some(EMPTY_REQUESTS_HASH);
// If Amsterdam is activated at genesis we set block access list hash to empty hash.
let block_access_list_hash = hardforks
.fork(EthereumHardfork::Amsterdam)
.active_at_timestamp(genesis.timestamp)
.then_some(EMPTY_BLOCK_ACCESS_LIST_HASH);
// If Amsterdam is activated at genesis we set slot number to 0.
let slot_number = hardforks
.fork(EthereumHardfork::Amsterdam)
.active_at_timestamp(genesis.timestamp)
.then_some(0);
Header {
number: genesis.number.unwrap_or_default(),
parent_hash: genesis.parent_hash.unwrap_or_default(),
@@ -93,6 +105,8 @@ pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Hea
blob_gas_used,
excess_blob_gas,
requests_hash,
block_access_list_hash,
slot_number,
..Default::default()
}
}
@@ -298,6 +312,7 @@ pub fn create_chain_config(
cancun_time: timestamp(EthereumHardfork::Cancun),
prague_time: timestamp(EthereumHardfork::Prague),
osaka_time: timestamp(EthereumHardfork::Osaka),
amsterdam_time: timestamp(EthereumHardfork::Amsterdam),
bpo1_time: timestamp(EthereumHardfork::Bpo1),
bpo2_time: timestamp(EthereumHardfork::Bpo2),
bpo3_time: timestamp(EthereumHardfork::Bpo3),
@@ -880,6 +895,7 @@ impl From<Genesis> for ChainSpec {
(EthereumHardfork::Cancun.boxed(), genesis.config.cancun_time),
(EthereumHardfork::Prague.boxed(), genesis.config.prague_time),
(EthereumHardfork::Osaka.boxed(), genesis.config.osaka_time),
(EthereumHardfork::Amsterdam.boxed(), genesis.config.amsterdam_time),
(EthereumHardfork::Bpo1.boxed(), genesis.config.bpo1_time),
(EthereumHardfork::Bpo2.boxed(), genesis.config.bpo2_time),
(EthereumHardfork::Bpo3.boxed(), genesis.config.bpo3_time),
@@ -1191,6 +1207,19 @@ impl ChainSpecBuilder {
self
}
/// Enable Amsterdam at genesis.
pub fn amsterdam_activated(mut self) -> Self {
self = self.osaka_activated();
self.hardforks.insert(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(0));
self
}
/// Enable Amsterdam at the given timestamp.
pub fn with_amsterdam_at(mut self, timestamp: u64) -> Self {
self.hardforks.insert(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(timestamp));
self
}
/// Build the resulting [`ChainSpec`].
///
/// # Panics

View File

@@ -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, features = ["blocking"] }
reqwest.workspace = true
url.workspace = true
metrics.workspace = true
blake3.workspace = true

View File

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

View File

@@ -8,9 +8,8 @@ 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;
use std::{hash::Hasher, time::Instant};
use tracing::info;
/// RocksDB tables that can be checksummed.

View File

@@ -22,7 +22,6 @@ mod settings;
mod stage_checkpoints;
mod state;
mod static_file_header;
mod static_files;
mod stats;
/// DB List TUI
mod tui;
@@ -64,8 +63,6 @@ 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
@@ -191,11 +188,6 @@ 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

@@ -1,31 +0,0 @@
//! 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

@@ -1,878 +0,0 @@
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,10 +3,9 @@ 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;
use std::{path::PathBuf, time::Instant};
use tracing::{info, warn};
/// Generate modular chunk archives and a snapshot manifest from a source datadir.

View File

@@ -84,10 +84,6 @@ 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>,
}
@@ -112,7 +108,6 @@ 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,
}
}
@@ -126,11 +121,10 @@ impl DownloadDefaults {
return custom_help.clone();
}
let mut help = format!(
let mut help = String::from(
"Specify a snapshot URL or let the command propose a default one.\n\n\
Browse available snapshots at {}\n\
Browse available snapshots at https://snapshots.reth.rs\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 {
@@ -175,12 +169,6 @@ 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());
@@ -203,7 +191,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 with --list-snapshots.
/// Browse available snapshots at <https://snapshots.reth.rs> or use --list-snapshots.
#[arg(long, short, long_help = DownloadDefaults::get_global().long_help())]
url: Option<String>,
@@ -273,7 +261,7 @@ pub struct DownloadCommand<C: ChainSpecParser> {
#[arg(long, default_value_t = MAX_CONCURRENT_DOWNLOADS)]
download_concurrency: usize,
/// List available snapshots and exit.
/// List available snapshots from snapshots.reth.rs and exit.
///
/// Queries the snapshots API and prints all available snapshots for the selected chain,
/// including block number, size, and manifest URL.
@@ -1340,17 +1328,7 @@ fn streaming_download_and_extract(
let response = match client.get(url).send().and_then(|r| r.error_for_status()) {
Ok(r) => r,
Err(e) => {
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);
last_error = Some(e.into());
if attempt < MAX_DOWNLOAD_RETRIES {
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
}
@@ -1377,15 +1355,6 @@ 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));
@@ -1551,7 +1520,6 @@ 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);
@@ -1559,31 +1527,13 @@ 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 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 (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 _ = 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,
@@ -1603,13 +1553,6 @@ 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,
@@ -1665,11 +1608,10 @@ fn file_blake3_hex(path: &Path) -> Result<String> {
/// Discovers the latest snapshot manifest URL for the given chain from the snapshots API.
///
/// Queries the configured snapshot API and returns the manifest URL for the most
/// Queries `snapshots.reth.rs/api/snapshots` 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 defaults = DownloadDefaults::get_global();
let api_url = &*defaults.snapshot_api_url;
let api_url = RETH_SNAPSHOTS_API_URL;
info!(target: "reth::cli", %api_url, %chain_id, "Discovering latest snapshot manifest");
@@ -1682,9 +1624,8 @@ 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- {}\n\n\
Use --list to see all available snapshots.",
api_url.trim_end_matches("/api/snapshots"),
\t- https://snapshots.reth.rs\n\n\
Use --list to see all available snapshots."
)
})?;
@@ -1715,7 +1656,7 @@ where
}
}
/// An entry from the snapshot discovery API listing.
/// An entry from the `snapshots.reth.rs/api/snapshots` listing.
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct SnapshotApiEntry {
@@ -1740,7 +1681,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 = &*DownloadDefaults::get_global().snapshot_api_url;
let api_url = RETH_SNAPSHOTS_API_URL;
let entries: Vec<SnapshotApiEntry> = Client::new()
.get(api_url)
@@ -1758,11 +1699,7 @@ 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();
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!("Available snapshots for chain {chain_id} (https://snapshots.reth.rs):\n");
println!("{:<12} {:>10} {:<10} {:>10} MANIFEST URL", "DATE", "BLOCK", "PROFILE", "SIZE");
println!("{}", "-".repeat(100));
@@ -1801,18 +1738,14 @@ 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{sources}"
Available snapshot sources:\n\
\t- https://snapshots.reth.rs\n\
\t- https://publicnode.com/snapshots"
)
})?;
Ok(response.json().await?)

View File

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

View File

@@ -179,8 +179,10 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
}
};
let bal= executor.take_bal();
if let Err(err) = consensus
.validate_block_post_execution(&block, &result, None)
.validate_block_post_execution(&block, &result, None, bal, true)
.wrap_err_with(|| {
format!(
"Failed to validate block {} {}",

View File

@@ -6,7 +6,7 @@ use reth_db_api::{
};
use reth_db_common::DbTool;
use reth_evm::ConfigureEvm;
use reth_node_api::HeaderTy;
use reth_node_builder::NodeTypesWithDB;
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: ProviderNodeTypes>(
fn import_tables_with_range<N: NodeTypesWithDB>(
output_db: &DatabaseEnv,
db_tool: &DbTool<N>,
from: u64,
@@ -74,7 +74,7 @@ fn import_tables_with_range<N: ProviderNodeTypes>(
)
})??;
output_db.update(|tx| {
tx.import_table_with_range::<tables::Headers<HeaderTy<N>>, _>(
tx.import_table_with_range::<tables::Headers, _>(
&db_tool.provider_factory.db_ref().tx()?,
Some(from),
to,

View File

@@ -10,7 +10,6 @@ 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},
@@ -42,7 +41,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<HeaderTy<N>>, _>(
tx.import_table_with_range::<tables::Headers, _>(
&db_tool.provider_factory.db_ref().tx()?,
Some(from),
to,

View File

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

View File

@@ -33,21 +33,19 @@ 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",
"dep:tikv-jemalloc-sys",
"tikv-jemallocator?/override_allocator_on_supported_platforms",
]
jemalloc = ["dep:tikv-jemallocator"]
# 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,11 +15,6 @@ 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,8 +10,6 @@
#[cfg(feature = "tracy-allocator")]
use reth_tracing as _;
#[cfg(feature = "tracy-allocator")]
use tracy_client as _;
pub mod allocator;
pub mod cancellation;

View File

@@ -87,6 +87,27 @@ pub fn validate_cancun_gas<B: Block>(block: &SealedBlock<B>) -> Result<(), Conse
Ok(())
}
/// Validate that Amsterdam header fields are present in the block.
///
/// This checks that the `block_access_list_hash` and `slot_number` are set in the header,
/// as required post-Amsterdam.
///
/// See [EIP-7928]: Block-level Access Lists
/// See [EIP-7778]: Slot Number in Block Header
///
/// [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
/// [EIP-7778]: https://eips.ethereum.org/EIPS/eip-7778
#[inline]
pub fn validate_amsterdam_header_fields<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
if header.block_access_list_hash().is_none() {
return Err(ConsensusError::BlockAccessListHashMissing);
}
if header.slot_number().is_none() {
return Err(ConsensusError::SlotNumberMissing);
}
Ok(())
}
/// Ensures the block response data matches the header.
///
/// This ensures the body response items match the header's hashes:

View File

@@ -18,6 +18,7 @@ reth-primitives-traits.workspace = true
# ethereum
alloy-primitives.workspace = true
alloy-consensus.workspace = true
alloy-eip7928.workspace = true
# misc
auto_impl.workspace = true
@@ -29,10 +30,8 @@ std = [
"reth-primitives-traits/std",
"alloy-primitives/std",
"alloy-consensus/std",
"reth-primitives-traits/std",
"alloy-eip7928/std",
"reth-execution-types/std",
"thiserror/std",
]
test-utils = [
"reth-primitives-traits/test-utils",
]
test-utils = ["reth-primitives-traits/test-utils"]

View File

@@ -1,23 +1,4 @@
//! 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",
@@ -32,6 +13,7 @@ extern crate alloc;
use alloc::{boxed::Box, fmt::Debug, string::String, sync::Arc, vec::Vec};
use alloy_consensus::Header;
use alloy_eip7928::BlockAccessList;
use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256};
use core::error::Error;
@@ -73,12 +55,17 @@ pub trait FullConsensus<N: NodePrimitives>: Consensus<N::Block> {
/// If `receipt_root_bloom` is provided, the implementation should use the pre-computed
/// receipt root and logs bloom instead of computing them from the receipts.
///
/// If `allow_bal_check` is enabled, we calculate the bal hash and match with the header bal
/// hash. We don't do by default because for payload validation, we do the same bal check
///
/// Note: validating blocks does not include other validations of the Consensus
fn validate_block_post_execution(
&self,
block: &RecoveredBlock<N::Block>,
result: &BlockExecutionResult<N::Receipt>,
receipt_root_bloom: Option<ReceiptRootBloom>,
block_access_list: Option<BlockAccessList>,
allow_bal_check: bool,
) -> Result<(), ConsensusError>;
}
@@ -350,6 +337,30 @@ pub enum ConsensusError {
#[error("unexpected parent beacon block root")]
ParentBeaconBlockRootUnexpected,
/// Error when the block access list hash is missing.
#[error("missing block access list hash")]
BlockAccessListHashMissing,
/// Error when an unexpected block access list hash is encountered.
#[error("unexpected block access list hash")]
BlockAccessListHashUnexpected,
/// Error when an unexpected block access list cost is encountered.
#[error("block access list cost exceeds gas limit")]
BlockAccessListCostMoreThanGasLimit,
/// Error when the block access list hash doesn't match the expected value.
#[error("block access list hash mismatch: {0}")]
BlockAccessListHashMismatch(GotExpectedBoxed<B256>),
/// Error when the slot number is missing.
#[error("missing slot number")]
SlotNumberMissing,
/// Error when an unexpected slot number is encountered.
#[error("unexpected slot number")]
SlotNumberUnexpected,
/// Error when blob gas used exceeds the maximum allowed.
#[error("blob gas used {blob_gas_used} exceeds maximum allowance {max_blob_gas_per_block}")]
BlobGasUsedExceedsMaxBlobGasPerBlock {

View File

@@ -20,6 +20,7 @@
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
use alloc::sync::Arc;
use alloy_eip7928::BlockAccessList;
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
@@ -77,6 +78,8 @@ impl<N: NodePrimitives> FullConsensus<N> for NoopConsensus {
_block: &RecoveredBlock<N::Block>,
_result: &BlockExecutionResult<N::Receipt>,
_receipt_root_bloom: Option<ReceiptRootBloom>,
_block_access_list: Option<BlockAccessList>,
_allow_bal_check: bool,
) -> Result<(), ConsensusError> {
Ok(())
}

View File

@@ -1,4 +1,5 @@
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
use alloy_eip7928::BlockAccessList;
use core::sync::atomic::{AtomicBool, Ordering};
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
@@ -52,6 +53,8 @@ impl<N: NodePrimitives> FullConsensus<N> for TestConsensus {
_block: &RecoveredBlock<N::Block>,
_result: &BlockExecutionResult<N::Receipt>,
_receipt_root_bloom: Option<ReceiptRootBloom>,
_block_access_list: Option<BlockAccessList>,
_allow_bal_check: bool,
) -> Result<(), ConsensusError> {
if self.fail_validation() {
Err(ConsensusError::BaseFeeMissing)

View File

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

View File

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

View File

@@ -79,16 +79,10 @@ where
target: "consensus::debug-client",
%err,
url=%self.url,
"Failed to subscribe to blocks, retrying in 5s",
"Failed to subscribe to blocks",
);
}) else {
// 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
return
};
while let Some(res) = stream.next().await {

View File

@@ -1,6 +1,5 @@
//! 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};
@@ -49,11 +48,7 @@ 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>::PayloadAttributes
+ Send
+ Sync
+ Copy
+ 'static,
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
) -> eyre::Result<(Vec<NodeHelperType<N>>, Wallet)>
where
N: NodeBuilderHelper,
@@ -70,11 +65,7 @@ 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>::PayloadAttributes
+ Send
+ Sync
+ Copy
+ 'static,
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
) -> eyre::Result<(
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
Wallet,
@@ -99,11 +90,7 @@ 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>::PayloadAttributes
+ Send
+ Sync
+ Copy
+ 'static,
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
connect_nodes: bool,
) -> eyre::Result<(
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
@@ -146,8 +133,11 @@ pub type NodeHelperType<N, Provider = BlockchainProvider<NodeTypesWithDBAdapter<
pub trait NodeBuilderHelper
where
Self: Default
+ NodeTypesForProvider<Payload: PayloadTypes<PayloadAttributes: From<PayloadAttributes>>>
+ Node<
+ NodeTypesForProvider<
Payload: PayloadTypes<
PayloadBuilderAttributes: From<reth_payload_builder::EthPayloadBuilderAttributes>,
>,
> + Node<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
ComponentsBuilder: NodeComponentsBuilder<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
@@ -168,8 +158,11 @@ where
impl<T> NodeBuilderHelper for T where
Self: Default
+ NodeTypesForProvider<Payload: PayloadTypes<PayloadAttributes: From<PayloadAttributes>>>
+ Node<
+ NodeTypesForProvider<
Payload: PayloadTypes<
PayloadBuilderAttributes: From<reth_payload_builder::EthPayloadBuilderAttributes>,
>,
> + Node<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
ComponentsBuilder: NodeComponentsBuilder<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,

View File

@@ -9,10 +9,13 @@ 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, FullNodeComponents, PayloadTypes, PrimitivesTy};
use reth_node_api::{
Block, BlockBody, BlockTy, EngineApiMessageVersion, FullNodeComponents, PayloadTypes,
PrimitivesTy,
};
use reth_node_builder::{rpc::RethRpcAddOns, FullNode, NodeTypes};
use reth_payload_primitives::BuiltPayload;
use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes};
use reth_provider::{
BlockReader, BlockReaderIdExt, CanonStateNotificationStream, CanonStateSubscriptions,
HeaderProvider, StageCheckpointReader,
@@ -55,7 +58,7 @@ where
/// Creates a new test node
pub async fn new(
node: FullNode<Node, AddOns>,
attributes_generator: impl Fn(u64) -> Payload::PayloadAttributes + Send + Sync + 'static,
attributes_generator: impl Fn(u64) -> Payload::PayloadBuilderAttributes + Send + Sync + 'static,
) -> eyre::Result<Self> {
Ok(Self {
inner: node.clone(),
@@ -103,50 +106,17 @@ 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> {
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();
// trigger new payload building draining the pool
let eth_attr = self.payload.new_payload().await.unwrap();
// first event is the payload attributes
self.payload.expect_attr_event(eth_attr).await?;
self.payload.expect_attr_event(eth_attr.clone()).await?;
// wait for the payload builder to have finished building
self.payload.wait_for_built_payload(payload_id).await;
self.payload.wait_for_built_payload(eth_attr.payload_id()).await;
// ensure we're also receiving the built payload as event
Ok(self.payload.expect_built_payload().await?)
}
@@ -295,6 +265,7 @@ 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, PayloadAttributes, PayloadKind};
use reth_node_api::{BlockBody, PayloadKind};
use reth_payload_builder::{PayloadBuilderHandle, PayloadId};
use reth_payload_builder_primitives::Events;
use reth_payload_primitives::{BuiltPayload, PayloadTypes};
use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes, 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::PayloadAttributes + Send + Sync>,
attributes_generator: Box<dyn Fn(u64) -> T::PayloadBuilderAttributes + 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::PayloadAttributes + Send + Sync + 'static,
attributes_generator: impl Fn(u64) -> T::PayloadBuilderAttributes + Send + Sync + 'static,
) -> eyre::Result<Self> {
let payload_events = payload_builder.subscribe().await?;
let payload_event_stream = payload_events.into_stream();
@@ -32,14 +32,19 @@ impl<T: PayloadTypes> PayloadTestContext<T> {
})
}
/// Generates the next payload attributes
pub fn next_attributes(&mut self) -> T::PayloadAttributes {
/// Creates a new payload job from static attributes
pub async fn new_payload(&mut self) -> eyre::Result<T::PayloadBuilderAttributes> {
self.timestamp += 1;
(self.attributes_generator)(self.timestamp)
let attributes = (self.attributes_generator)(self.timestamp);
self.payload_builder.send_new_payload(attributes.clone()).await.unwrap()?;
Ok(attributes)
}
/// Asserts that the next event is a payload attributes event
pub async fn expect_attr_event(&mut self, attrs: T::PayloadAttributes) -> eyre::Result<()> {
pub async fn expect_attr_event(
&mut self,
attrs: T::PayloadBuilderAttributes,
) -> 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>::PayloadAttributes
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
+ 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>::PayloadAttributes
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
+ 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>::PayloadAttributes
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
+ Send
+ Sync
+ Copy

View File

@@ -1,7 +1,6 @@
//! 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;
@@ -60,7 +59,11 @@ pub async fn setup_engine_with_chain_import(
is_dev: bool,
tree_config: TreeConfig,
rlp_path: &Path,
attributes_generator: impl Fn(u64) -> PayloadAttributes + Send + Sync + Copy + 'static,
attributes_generator: impl Fn(u64) -> reth_payload_builder::EthPayloadBuilderAttributes
+ Send
+ Sync
+ Copy
+ 'static,
) -> eyre::Result<ChainImportResult> {
let runtime = reth_tasks::Runtime::test();
@@ -270,10 +273,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,
@@ -566,7 +569,7 @@ mod tests {
false,
TreeConfig::default(),
&rlp_path,
|_| PayloadAttributes::default(),
|_| EthPayloadBuilderAttributes::default(),
)
.await
.expect("Failed to setup nodes with chain import");

View File

@@ -55,6 +55,8 @@ pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlo
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
block_access_list_hash: None,
slot_number: None,
};
// Set required fields based on chain spec

View File

@@ -227,6 +227,7 @@ where
suggested_fee_recipient: alloy_primitives::Address::random(),
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
slot_number: None,
};
env.active_node_state_mut()?
@@ -299,6 +300,7 @@ where
suggested_fee_recipient: alloy_primitives::Address::random(),
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
slot_number: None,
};
let fresh_fcu_result = EngineApiClient::<Engine>::fork_choice_updated_v3(

View File

@@ -10,6 +10,7 @@ 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::{
@@ -263,12 +264,16 @@ where
let chain_spec =
self.chain_spec.clone().ok_or_else(|| eyre!("Chain specification is required"))?;
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),
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),
slot_number: None,
};
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
};
crate::setup_import::setup_engine_with_chain_import(
@@ -284,19 +289,24 @@ 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>::PayloadAttributes + Copy + use<N, I>
) -> impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
+ Copy
+ use<N, I>
where
N: NodeBuilderHelper<Payload = I>,
{
move |timestamp| {
PayloadAttributes {
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),
}
.into()
slot_number: None,
};
<<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes::from(
EthPayloadBuilderAttributes::new(B256::ZERO, attributes),
)
}
}

View File

@@ -19,6 +19,7 @@ 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;
@@ -160,6 +161,7 @@ async fn test_testsuite_assert_mine_block() -> Result<()> {
suggested_fee_recipient: Address::random(),
withdrawals: None,
parent_beacon_block_root: None,
slot_number: None,
},
));
@@ -370,7 +372,7 @@ async fn test_setup_builder_with_custom_tree_config() -> Result<()> {
);
let (nodes, _wallet) = E2ETestSetupBuilder::<EthereumNode, _>::new(1, chain_spec, |_| {
PayloadAttributes::default()
EthPayloadBuilderAttributes::default()
})
.with_tree_config_modifier(|config| {
config.with_persistence_threshold(0).with_memory_block_buffer_target(5)

View File

@@ -3,7 +3,6 @@
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;
@@ -11,6 +10,7 @@ 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,14 +83,16 @@ fn test_chain_spec() -> Arc<ChainSpec> {
}
/// Returns test payload attributes for the given timestamp.
const fn test_attributes_generator(timestamp: u64) -> PayloadAttributes {
PayloadAttributes {
fn test_attributes_generator(timestamp: u64) -> EthPayloadBuilderAttributes {
let attributes = alloy_rpc_types_engine::PayloadAttributes {
timestamp,
prev_randao: B256::ZERO,
suggested_fee_recipient: alloy_primitives::Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
}
slot_number: None,
};
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
}
/// Smoke test: node boots with `RocksDB` routing enabled.

View File

@@ -1,39 +0,0 @@
[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

@@ -1,227 +0,0 @@
//! 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,6 +32,14 @@ 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,7 +6,9 @@ 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, PayloadAttributesBuilder, PayloadKind, PayloadTypes};
use reth_payload_primitives::{
BuiltPayload, EngineApiMessageVersion, PayloadAttributesBuilder, PayloadKind, PayloadTypes,
};
use reth_primitives_traits::{HeaderTy, SealedHeaderFor};
use reth_storage_api::BlockReader;
use reth_transaction_pool::TransactionPool;
@@ -212,7 +214,10 @@ 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).await?;
let res = self
.to_engine
.fork_choice_updated(state, None, EngineApiMessageVersion::default())
.await?;
if !res.is_valid() {
eyre::bail!("Invalid fork choice update {state:?}: {res:?}")
@@ -229,6 +234,7 @@ where
.fork_choice_updated(
self.forkchoice_state(),
Some(self.payload_attributes_builder.build(&self.last_header)),
EngineApiMessageVersion::default(),
)
.await?;

View File

@@ -57,6 +57,60 @@ where
.chain_spec
.is_cancun_active_at_timestamp(timestamp)
.then(B256::random),
slot_number: None,
}
}
}
#[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,9 +6,6 @@ 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;
@@ -47,16 +44,6 @@ 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
@@ -95,8 +82,6 @@ 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,
@@ -166,10 +151,17 @@ 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,
/// Whether to disable BAL (Block Access List, EIP-7928) based parallel execution.
/// When disabled, falls back to transaction-based prewarming even when a BAL is available.
disable_bal_parallel_execution: bool,
/// Whether to disable BAL-driven parallel state root computation.
/// When disabled, the BAL hashed post state is not sent to the multiproof task for
/// early parallel state root computation.
disable_bal_parallel_state_root: bool,
/// Whether to disable BAL (Block Access List) batched IO during prewarming.
/// When disabled, falls back to individual per-slot storage reads instead of
/// batched cursor reads via `storage_range`.
disable_bal_batch_io: 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.
@@ -179,14 +171,9 @@ 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,
@@ -210,8 +197,9 @@ 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,
disable_bal_parallel_execution: false,
disable_bal_parallel_state_root: false,
disable_bal_batch_io: false,
#[cfg(feature = "trie-debug")]
proof_jitter: None,
}
@@ -224,7 +212,6 @@ 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,
@@ -247,17 +234,10 @@ 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,
@@ -281,8 +261,9 @@ 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,
disable_bal_parallel_execution: false,
disable_bal_parallel_state_root: false,
disable_bal_batch_io: false,
#[cfg(feature = "trie-debug")]
proof_jitter: None,
}
@@ -298,11 +279,6 @@ 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
@@ -399,10 +375,6 @@ 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
}
@@ -415,19 +387,6 @@ 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;
@@ -617,32 +576,31 @@ 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 BAL-based parallel execution is disabled.
pub const fn disable_bal_parallel_execution(&self) -> bool {
self.disable_bal_parallel_execution
}
/// 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(
/// Setter for whether to disable BAL-based parallel execution.
pub const fn without_bal_parallel_execution(
mut self,
share_execution_cache_with_payload_builder: bool,
disable_bal_parallel_execution: bool,
) -> Self {
self.share_execution_cache_with_payload_builder =
share_execution_cache_with_payload_builder;
self.disable_bal_parallel_execution = disable_bal_parallel_execution;
self
}
/// Setter for whether to share sparse trie with the payload builder.
pub const fn with_share_sparse_trie_with_payload_builder(
/// Returns whether BAL-driven parallel state root computation is disabled.
pub const fn disable_bal_parallel_state_root(&self) -> bool {
self.disable_bal_parallel_state_root
}
/// Setter for whether to disable BAL-driven parallel state root computation.
pub const fn without_bal_parallel_state_root(
mut self,
share_sparse_trie_with_payload_builder: bool,
disable_bal_parallel_state_root: bool,
) -> Self {
self.share_sparse_trie_with_payload_builder = share_sparse_trie_with_payload_builder;
self.disable_bal_parallel_state_root = disable_bal_parallel_state_root;
self
}
@@ -658,19 +616,15 @@ impl TreeConfig {
self.proof_jitter = proof_jitter;
self
}
}
#[cfg(test)]
mod tests {
use super::TreeConfig;
/// Returns whether BAL batched IO is disabled.
pub const fn disable_bal_batch_io(&self) -> bool {
self.disable_bal_batch_io
}
#[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);
/// Setter for whether to disable BAL batched IO.
pub const fn without_bal_batch_io(mut self, disable_bal_batch_io: bool) -> Self {
self.disable_bal_batch_io = disable_bal_batch_io;
self
}
}

View File

@@ -86,14 +86,13 @@ where
}
}
/// Information about a slow block detected after execution or persistence.
/// Information about a slow block detected after 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.
/// `None` when emitted immediately after execution (before persistence).
pub commit_duration: Option<Duration>,
pub commit_duration: 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,19 +117,7 @@ pub trait EngineTypes:
+ 'static;
}
/// 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).
/// Type that validates the payloads processed by the engine API.
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.
@@ -148,34 +136,6 @@ 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.
@@ -231,11 +191,6 @@ 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::PayloadTypes;
use reth_payload_primitives::{EngineApiMessageVersion, PayloadTypes};
use std::time::Duration;
use tokio::sync::{mpsc::UnboundedSender, oneshot};
@@ -162,30 +162,6 @@ 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)]
@@ -219,6 +195,8 @@ 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>>,
},
@@ -319,9 +297,10 @@ where
&self,
state: ForkchoiceState,
payload_attrs: Option<Payload::PayloadAttributes>,
version: EngineApiMessageVersion,
) -> Result<ForkchoiceUpdated, BeaconForkChoiceUpdateError> {
Ok(self
.send_fork_choice_updated(state, payload_attrs)
.send_fork_choice_updated(state, payload_attrs, version)
.map_err(|_| BeaconForkChoiceUpdateError::EngineUnavailable)
.await?
.map_err(BeaconForkChoiceUpdateError::internal)?
@@ -334,12 +313,14 @@ 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,7 +17,6 @@ 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"] }
@@ -53,6 +52,7 @@ 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
@@ -60,6 +60,7 @@ metrics.workspace = true
reth-metrics = { workspace = true, features = ["common"] }
# misc
itertools.workspace = true
schnellru.workspace = true
rayon.workspace = true
tracing.workspace = true
@@ -132,7 +133,6 @@ 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,
ProviderFactory, StorageSettingsCache,
};
use reth_prune::PrunerWithFactory;
use reth_stages_api::{MetricEventsSender, Pipeline};
@@ -81,6 +81,7 @@ 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);
@@ -98,6 +99,7 @@ where
engine_kind,
evm_config,
changeset_cache,
use_hashed_state,
runtime,
);

View File

@@ -6,7 +6,7 @@ use alloy_primitives::{
use fixed_cache::{AnyRef, CacheConfig, Stats, StatsHandler};
use metrics::{Counter, Gauge, Histogram};
use parking_lot::Once;
use reth_errors::ProviderResult;
use reth_errors::{ProviderError, ProviderResult};
use reth_metrics::Metrics;
use reth_primitives_traits::{Account, Bytecode};
use reth_provider::{
@@ -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` also caches
/// populated because the actual EVM database [`State`](revm::database::State) also caches
/// internally during block execution and the cache is then updated after the block with the entire
/// [`BundleState`] output of that block which contains all accessed accounts, code, storage. See
/// also [`ExecutionCache::insert_state`].
@@ -230,7 +230,7 @@ impl CachedStateMetrics {
}
/// Records a new execution cache creation with its duration.
pub fn record_cache_creation(&self, duration: Duration) {
pub(crate) 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,63 +254,51 @@ pub struct CacheStats {
}
impl CacheStats {
/// Records an account cache hit.
pub fn record_account_hit(&self) {
pub(crate) fn record_account_hit(&self) {
self.account_hits.fetch_add(1, Ordering::Relaxed);
}
/// Records an account cache miss.
pub fn record_account_miss(&self) {
pub(crate) fn record_account_miss(&self) {
self.account_misses.fetch_add(1, Ordering::Relaxed);
}
/// Returns the number of account cache hits.
pub fn account_hits(&self) -> usize {
pub(crate) fn account_hits(&self) -> usize {
self.account_hits.load(Ordering::Relaxed)
}
/// Returns the number of account cache misses.
pub fn account_misses(&self) -> usize {
pub(crate) fn account_misses(&self) -> usize {
self.account_misses.load(Ordering::Relaxed)
}
/// Records a storage cache hit.
pub fn record_storage_hit(&self) {
pub(crate) fn record_storage_hit(&self) {
self.storage_hits.fetch_add(1, Ordering::Relaxed);
}
/// Records a storage cache miss.
pub fn record_storage_miss(&self) {
pub(crate) fn record_storage_miss(&self) {
self.storage_misses.fetch_add(1, Ordering::Relaxed);
}
/// Returns the number of storage cache hits.
pub fn storage_hits(&self) -> usize {
pub(crate) fn storage_hits(&self) -> usize {
self.storage_hits.load(Ordering::Relaxed)
}
/// Returns the number of storage cache misses.
pub fn storage_misses(&self) -> usize {
pub(crate) fn storage_misses(&self) -> usize {
self.storage_misses.load(Ordering::Relaxed)
}
/// Records a code cache hit.
pub fn record_code_hit(&self) {
pub(crate) fn record_code_hit(&self) {
self.code_hits.fetch_add(1, Ordering::Relaxed);
}
/// Records a code cache miss.
pub fn record_code_miss(&self) {
pub(crate) fn record_code_miss(&self) {
self.code_misses.fetch_add(1, Ordering::Relaxed);
}
/// Returns the number of code cache hits.
pub fn code_hits(&self) -> usize {
pub(crate) fn code_hits(&self) -> usize {
self.code_hits.load(Ordering::Relaxed)
}
/// Returns the number of code cache misses.
pub fn code_misses(&self) -> usize {
pub(crate) fn code_misses(&self) -> usize {
self.code_misses.load(Ordering::Relaxed)
}
}
@@ -330,7 +318,7 @@ impl CacheStats {
///
/// Collisions (evicting a different key) don't change size since they replace an existing entry.
#[derive(Debug)]
pub struct CacheStatsHandler {
pub(crate) struct CacheStatsHandler {
collisions: AtomicU64,
size: AtomicUsize,
capacity: usize,
@@ -338,42 +326,42 @@ pub struct CacheStatsHandler {
impl CacheStatsHandler {
/// Creates a new stats handler with all counters initialized to zero.
pub const fn new(capacity: usize) -> Self {
pub(crate) const fn new(capacity: usize) -> Self {
Self { collisions: AtomicU64::new(0), size: AtomicUsize::new(0), capacity }
}
/// Returns the number of cache collisions.
pub fn collisions(&self) -> u64 {
pub(crate) fn collisions(&self) -> u64 {
self.collisions.load(Ordering::Relaxed)
}
/// Returns the current size (number of entries).
pub fn size(&self) -> usize {
pub(crate) fn size(&self) -> usize {
self.size.load(Ordering::Relaxed)
}
/// Returns the capacity (maximum number of entries).
pub const fn capacity(&self) -> usize {
pub(crate) const fn capacity(&self) -> usize {
self.capacity
}
/// Increments the size counter. Called on cache insert.
pub fn increment_size(&self) {
pub(crate) fn increment_size(&self) {
let _ = self.size.fetch_add(1, Ordering::Relaxed);
}
/// Decrements the size counter. Called on cache remove.
pub fn decrement_size(&self) {
pub(crate) fn decrement_size(&self) {
let _ = self.size.fetch_sub(1, Ordering::Relaxed);
}
/// Resets size to zero. Called on cache clear.
pub fn reset_size(&self) {
pub(crate) fn reset_size(&self) {
self.size.store(0, Ordering::Relaxed);
}
/// Resets collision counter to zero (but not size).
pub fn reset_stats(&self) {
pub(crate) fn reset_stats(&self) {
self.collisions.store(0, Ordering::Relaxed);
}
}
@@ -478,6 +466,55 @@ impl<S: StateProvider, const PREWARM: bool> StateProvider for CachedStateProvide
self.state_provider.storage(account, storage_key)
}
}
fn storage_range(
&self,
account: Address,
keys: &[StorageKey],
) -> ProviderResult<Vec<(StorageKey, StorageValue)>> {
let mut uncached_keys = Vec::new();
let mut result = Vec::with_capacity(keys.len());
for &key in keys {
if let Some(value) = self.caches.get_storage(account, key) {
if !value.is_zero() {
result.push((key, value));
}
} else {
uncached_keys.push(key);
}
}
// Batch-fetch all uncached keys from the inner provider
if !uncached_keys.is_empty() {
let mut fetched = self.state_provider.storage_range(account, &uncached_keys)?;
// Sort by raw key to align with uncached_keys for the merge-join below.
// The inner provider may return results in a different order (e.g. hashed state
// iterates by hashed slot).
fetched.sort_unstable_by_key(|(k, _)| *k);
// Merge-join to find zero slots without allocating a HashSet.
let mut fetched_iter = fetched.iter();
let mut next = fetched_iter.next();
for &key in &uncached_keys {
if let Some(&(fk, fv)) = next &&
fk == key
{
let _ = self.caches.get_or_try_insert_storage_with(account, key, || {
Ok::<_, ProviderError>(fv)
});
result.push((key, fv));
next = fetched_iter.next();
continue;
}
// key not returned by inner provider → zero slot
let _ = self.caches.get_or_try_insert_storage_with(account, key, || {
Ok::<_, ProviderError>(StorageValue::ZERO)
});
}
}
Ok(result)
}
}
impl<S: BytecodeReader, const PREWARM: bool> BytecodeReader for CachedStateProvider<S, PREWARM> {
@@ -768,18 +805,23 @@ impl ExecutionCache {
}
}
/// Returns a cached storage value if present, without inserting.
pub fn get_storage(&self, address: Address, key: StorageKey) -> Option<StorageValue> {
self.0.storage_cache.get(&(address, key))
}
/// Insert storage value into cache.
pub fn insert_storage(&self, address: Address, key: StorageKey, value: Option<StorageValue>) {
self.0.storage_cache.insert((address, key), value.unwrap_or_default());
}
/// Insert code into cache.
pub fn insert_code(&self, hash: B256, code: Option<Bytecode>) {
fn insert_code(&self, hash: B256, code: Option<Bytecode>) {
self.0.code_cache.insert(hash, code);
}
/// Insert account into cache.
pub fn insert_account(&self, address: Address, account: Option<Account>) {
fn insert_account(&self, address: Address, account: Option<Account>) {
self.0.account_cache.insert(address, account);
}
@@ -852,6 +894,7 @@ impl ExecutionCache {
}
self.0.account_cache.remove(addr);
self.0.account_stats.decrement_size();
continue;
}
@@ -880,7 +923,7 @@ impl ExecutionCache {
///
/// We do not clear the bytecodes cache, because its mapping can never change, as it's
/// `keccak256(bytecode) => bytecode`.
pub fn clear(&self) {
pub(crate) fn clear(&self) {
self.0.storage_cache.clear();
self.0.account_cache.clear();
@@ -890,7 +933,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 fn update_metrics(&self, metrics: &CachedStateMetrics) {
pub(crate) 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);
@@ -975,7 +1018,7 @@ impl SavedCache {
///
/// Note: This can be expensive with large cached state. Use
/// `with_disable_cache_metrics(true)` to skip.
pub fn update_metrics(&self) {
pub(crate) fn update_metrics(&self) {
if self.disable_cache_metrics {
return
}
@@ -984,16 +1027,15 @@ impl SavedCache {
/// Clears all caches, resetting them to empty state,
/// and updates the hash of the block this cache belongs to.
pub fn clear_with_hash(&mut self, hash: B256) {
pub(crate) fn clear_with_hash(&mut self, hash: B256) {
self.hash = hash;
self.caches.clear();
}
}
#[cfg(any(test, feature = "test-utils"))]
#[cfg(test)]
impl SavedCache {
/// Clones the usage guard for testing availability tracking.
pub fn clone_guard_for_test(&self) -> Arc<()> {
fn clone_guard_for_test(&self) -> Arc<()> {
self.usage_guard.clone()
}
}
@@ -1234,33 +1276,6 @@ 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

@@ -171,10 +171,6 @@ 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
@@ -452,17 +448,11 @@ impl NewPayloadStatusMetrics {
Ok(outcome) => match outcome.outcome.status {
PayloadStatusEnum::Valid => {
self.new_payload_valid.increment(1);
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);
}
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);
}
PayloadStatusEnum::Syncing => self.new_payload_syncing.increment(1),
PayloadStatusEnum::Accepted => self.new_payload_accepted.increment(1),
@@ -471,6 +461,9 @@ 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,8 +23,10 @@ use reth_engine_primitives::{
};
use reth_errors::{ConsensusError, ProviderResult};
use reth_evm::ConfigureEvm;
use reth_payload_builder::{BuildNewPayload, PayloadBuilderHandle};
use reth_payload_primitives::{BuiltPayload, NewPayloadError, PayloadTypes};
use reth_payload_builder::PayloadBuilderHandle;
use reth_payload_primitives::{
BuiltPayload, EngineApiMessageVersion, NewPayloadError, PayloadBuilderAttributes, PayloadTypes,
};
use reth_primitives_traits::{
FastInstant as Instant, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
};
@@ -50,6 +52,7 @@ use tokio::sync::{
use tracing::*;
mod block_buffer;
mod cached_state;
pub mod error;
pub mod instrumented_state;
mod invalid_headers;
@@ -64,15 +67,13 @@ 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;
@@ -179,15 +180,12 @@ 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, already_seen: false }
Self { outcome, event: None }
}
/// Set event on the outcome.
@@ -195,31 +193,6 @@ 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
@@ -304,6 +277,9 @@ 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,
}
@@ -332,6 +308,7 @@ 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()
}
@@ -372,6 +349,7 @@ 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();
@@ -395,6 +373,7 @@ where
evm_config,
changeset_cache,
execution_timing_stats: HashMap::new(),
use_hashed_state,
runtime,
}
}
@@ -416,6 +395,7 @@ 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>>)
{
@@ -449,6 +429,7 @@ where
kind,
evm_config,
changeset_cache,
use_hashed_state,
runtime,
);
let incoming = task.incoming_tx.clone();
@@ -472,79 +453,12 @@ 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 {
// 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 {
match self.wait_for_event() {
LoopEvent::EngineMessage(msg) => {
debug!(target: "engine::tree", %msg, "received new engine message");
match self.on_engine_message(msg) {
@@ -579,24 +493,6 @@ 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).
///
@@ -743,12 +639,13 @@ where
// record pre-execution phase duration
self.metrics.block_validation.record_payload_validation(start.elapsed().as_secs_f64());
let mut outcome = if self.backfill_sync_state.is_idle() {
self.try_insert_payload(payload)?.into_outcome()
let status = if self.backfill_sync_state.is_idle() {
self.try_insert_payload(payload)?
} else {
TreeOutcome::new(self.try_buffer_payload(payload)?)
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
@@ -766,11 +663,16 @@ 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<TryInsertPayloadResult, InsertBlockFatalError> {
) -> Result<PayloadStatus, InsertBlockFatalError> {
let block_hash = payload.block_hash();
let num_hash = payload.num_hash();
let parent_hash = payload.parent_hash();
@@ -778,40 +680,31 @@ where
match self.insert_payload(payload) {
Ok(status) => {
let (status, already_seen) = match status {
let status = match status {
InsertPayloadOk::Inserted(BlockStatus::Valid) => {
latest_valid_hash = Some(block_hash);
self.try_connect_buffered_blocks(num_hash)?;
(PayloadStatusEnum::Valid, false)
PayloadStatusEnum::Valid
}
InsertPayloadOk::AlreadySeen(BlockStatus::Valid) => {
latest_valid_hash = Some(block_hash);
(PayloadStatusEnum::Valid, true)
}
InsertPayloadOk::Inserted(BlockStatus::Disconnected { .. }) => {
(PayloadStatusEnum::Syncing, false)
PayloadStatusEnum::Valid
}
InsertPayloadOk::Inserted(BlockStatus::Disconnected { .. }) |
InsertPayloadOk::AlreadySeen(BlockStatus::Disconnected { .. }) => {
// not known to be invalid, but we don't know anything else
(PayloadStatusEnum::Syncing, true)
PayloadStatusEnum::Syncing
}
};
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 })
Ok(PayloadStatus::new(status, latest_valid_hash))
}
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)?)
}
},
}
}
@@ -1108,6 +1001,7 @@ where
&mut self,
state: ForkchoiceState,
attrs: Option<T::PayloadAttributes>,
version: EngineApiMessageVersion,
) -> ProviderResult<TreeOutcome<OnForkChoiceUpdated>> {
trace!(target: "engine::tree", ?attrs, "invoked forkchoice update");
@@ -1120,13 +1014,13 @@ where
}
// Return early if we are on the correct fork
if let Some(result) = self.handle_canonical_head(state, &attrs)? {
if let Some(result) = self.handle_canonical_head(state, &attrs, version)? {
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)? {
if let Some(result) = self.apply_chain_update(state, &attrs, version)? {
return Ok(result);
}
@@ -1177,6 +1071,7 @@ 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
//
@@ -1214,7 +1109,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);
let updated = self.process_payload_attributes(attr.clone(), &tip, state, version);
return Ok(Some(TreeOutcome::new(updated)));
}
@@ -1237,6 +1132,7 @@ 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) {
@@ -1259,8 +1155,12 @@ 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);
let updated = self.process_payload_attributes(
attr.clone(),
&canonical_header,
state,
version,
);
return Ok(Some(TreeOutcome::new(updated)));
}
}
@@ -1291,7 +1191,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);
let updated = self.process_payload_attributes(attr.clone(), &tip, state, version);
return Ok(Some(TreeOutcome::new(updated)));
}
@@ -1405,8 +1305,7 @@ 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() {
debug!(target: "engine::tree", ?action, "waiting for in-flight persistence");
if let Some((rx, start_time, _action)) = self.persistence_state.rx.take() {
let result = rx.recv().map_err(|_| AdvancePersistenceError::ChannelClosed)?;
self.on_persistence_complete(result, start_time)?;
}
@@ -1426,7 +1325,8 @@ where
/// Tries to poll for a completed persistence task (non-blocking).
///
/// Returns `true` if a persistence task was completed, `false` otherwise.
fn try_poll_persistence(&mut self) -> Result<bool, AdvancePersistenceError> {
#[cfg(test)]
pub fn try_poll_persistence(&mut self) -> Result<bool, AdvancePersistenceError> {
let Some((rx, start_time, action)) = self.persistence_state.rx.take() else {
return Ok(false);
};
@@ -1564,11 +1464,17 @@ where
}
EngineApiRequest::Beacon(request) => {
match request {
BeaconEngineMessage::ForkchoiceUpdated { state, payload_attrs, tx } => {
BeaconEngineMessage::ForkchoiceUpdated {
state,
payload_attrs,
tx,
version,
} => {
let has_attrs = payload_attrs.is_some();
let start = Instant::now();
let mut output = self.on_forkchoice_updated(state, payload_attrs);
let mut output =
self.on_forkchoice_updated(state, payload_attrs, version);
if let Ok(res) = &mut output {
// track last received forkchoice state
@@ -1964,7 +1870,7 @@ where
if total_duration > threshold.expect("checked above") {
self.emit_event(ConsensusEngineEvent::SlowBlock(SlowBlockInfo {
stats,
commit_duration: Some(commit_dur),
commit_duration: commit_dur,
total_duration,
}));
}
@@ -2613,7 +2519,12 @@ where
self.update_reorg_metrics(old.len(), old_first);
self.reinsert_reorged_blocks(new.clone());
self.reinsert_reorged_blocks(old.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());
}
}
// update the tracked in-memory state with the new chain
@@ -2777,7 +2688,10 @@ where
// try to append the block
match self.insert_block(block) {
Ok(InsertPayloadOk::Inserted(BlockStatus::Valid)) => {
Ok(
InsertPayloadOk::Inserted(BlockStatus::Valid) |
InsertPayloadOk::AlreadySeen(BlockStatus::Valid),
) => {
return self.on_valid_downloaded_block(block_num_hash);
}
Ok(InsertPayloadOk::Inserted(BlockStatus::Disconnected { head, missing_ancestor })) => {
@@ -2935,19 +2849,8 @@ where
let (executed, timing_stats) = execute(&mut self.payload_validator, input, ctx)?;
// Emit slow block event immediately after execution so it appears even when
// persistence hasn't completed yet (e.g. blocks arriving faster than persistence).
// Store timing stats for detailed block logging after 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);
}
@@ -3149,26 +3052,17 @@ 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,
attributes: T::PayloadAttributes,
attrs: T::PayloadAttributes,
head: &N::BlockHeader,
state: ForkchoiceState,
version: EngineApiMessageVersion,
) -> OnForkChoiceUpdated {
if let Err(err) =
self.payload_validator.validate_payload_attributes_against_header(&attributes, head)
self.payload_validator.validate_payload_attributes_against_header(&attrs, head)
{
warn!(target: "engine::tree", %err, ?head, "Invalid payload attributes");
return OnForkChoiceUpdated::invalid_payload_attributes()
@@ -3178,47 +3072,34 @@ 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);
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,
)
// 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(),
}
}
/// Remove all blocks up to __and including__ the given block number.

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