Compare commits

..

45 Commits

Author SHA1 Message Date
joshieDo
6d545d1bd5 refactor(snap): verify snap range proofs 2026-04-29 15:51:24 +01:00
joshieDo
35e901ea51 refactor(snap): tighten snap/2 state exchange
Use typed trie accounts in snap wire data, share unordered proof verification through trie-common, remove dead snap helpers, and restore eth message version gating while keeping snap/2 multiplexing separate.
2026-04-29 14:34:03 +01:00
joshieDo
af1b99f0d7 feat(snap): add snap/2 sync flow
Moves snap serving and lifecycle orchestration into the snap crate, removes snap/1 trie-node paths, routes snap/2 BAL requests, and writes snap state through provider APIs.

Also updates the snap E2E entrypoint and branch implementation notes.
2026-04-29 13:26:18 +01:00
joshieDo
a46ef1cd91 refactor(snap): structural cleanup — split modules and delete dead code
- Delete unused DbSnapStateProvider from e2e-test-utils/snap.rs
- Split engine/snap/download.rs into download.rs (network), storage.rs
  (MDBX helpers), finalize.rs (checkpoints + static files)
- Extract snap integration from tree/mod.rs into tree/snap.rs
  (forwarding helpers + on_snap_sync_finished)
- Update snapv2.md with current implementation state and refactoring plan

Amp-Thread-ID: https://ampcode.com/threads/T-019dd4cb-6f92-764a-861b-32ca89013b59
Co-authored-by: Amp <amp@ampcode.com>
2026-04-29 09:14:17 +01:00
joshieDo
7708ac59e7 fix(snap): advance static file segments after snap sync
After snap sync completes, advance Transactions, TransactionSenders,
Receipts, and changeset static file segments to the snap target block.
Without this the persistence service fails with
UnexpectedStaticFileBlockNumber when writing blocks after the snap target.

Also adds can_snap_sync_stale_pivot e2e test that exercises the
orchestrator's stale-pivot recovery path by pushing the pivot outside
the 128-block serving lookback window.

Amp-Thread-ID: https://ampcode.com/threads/T-019dd4cb-6f92-764a-861b-32ca89013b59
Co-authored-by: Amp <amp@ampcode.com>
2026-04-29 08:41:33 +01:00
joshieDo
f7863c766b wip 2026-04-28 16:53:34 +01:00
joshieDo
8324ee1173 Merge remote-tracking branch 'origin/bal-devnet-5' into snapv2
Amp-Thread-ID: https://ampcode.com/threads/T-019dd45f-d4b6-7778-bbdd-6d6392f70835
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	crates/net/eth-wire-types/src/version.rs
#	crates/net/network/src/builder.rs
#	crates/net/network/src/eth_requests.rs
#	crates/net/network/src/test_utils/testnet.rs
2026-04-28 16:11:56 +01:00
joshieDo
00f6c75f06 feat: snap/2 sync implementation (WIP)
Engine-driven snap sync with two-phase design:
- Phase A: HeaderStage downloads headers 1..pivot to static files
- Phase B: SnapSyncOrchestrator streams state + BAL catch-up

Includes: CombinedBackfillSync, SnapSyncController, pivot tracking,
BAL diff application, snap state provider with revert overlay,
storage v2 skip for move_to_static_files, E2E test scaffolding.

BAL caching in get_built_payload for serving node.
State root mismatch remains: BAL bytes not reaching BalStore
due to V3/V4 payload conversion (header lacks block_access_list_hash).

Amp-Thread-ID: https://ampcode.com/threads/T-019dd45f-d4b6-7778-bbdd-6d6392f70835
Co-authored-by: Amp <amp@ampcode.com>
2026-04-28 16:09:42 +01:00
Alexey Shekhirin
2fce3e701d bblock -> block 2026-04-28 16:44:31 +02:00
Alexey Shekhirin
938313028d Merge remote-tracking branch 'origin/main' into bal-devnet-5
# Conflicts:
#	Cargo.lock
2026-04-28 16:29:28 +02:00
Alexey Shekhirin
a8fc13dc25 deps: bump alloy-evm to 0.33.3 (#23778) 2026-04-28 16:07:13 +02:00
Matthias Seitz
4c1f6b6507 fix(payload): track Amsterdam block gas in builders (#23743) 2026-04-28 13:31:25 +00:00
RandoomWalks
b850f2a81d fix(net): apply count cap to BlockAccessLists request handler (#23754)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-04-28 12:57:59 +00:00
Alexey Shekhirin
0790359003 fix(consensus): BAL exceeds gas limit error message 2026-04-28 14:38:23 +02:00
Sergei Shulepov
79144cb430 fix(bench): reth-bb multiple executors (#23763) 2026-04-28 11:53:53 +00:00
Alexey Shekhirin
089c0e2629 Merge branch 'main' into bal-devnet-4 2026-04-28 12:52:01 +01:00
Alexey Shekhirin
b04346ffe5 feat(engine): disable BAL storage prefetch on CLI arg (#23770) 2026-04-28 11:40:53 +00:00
Alexey Shekhirin
674623f14e ci(hive): ignore more EIP-7610 tests (#23769) 2026-04-28 11:37:42 +00:00
Matthias Seitz
bc80e2a66b feat(net): enable ETH70 by default (#23768) 2026-04-28 13:31:12 +02:00
Alexey Shekhirin
9af8265047 allow to run hive.yml on reth-oss 2026-04-28 12:57:53 +02:00
Sergei Shulepov
97b1b56b2d fix(bench): dedupe merged BAL storage reads (#23758) 2026-04-28 10:39:31 +00:00
Soubhik Singha Mahapatra
f3e30a3111 Merge branch 'paradigmxyz:bal-devnet-4' into bal-devnet-4 2026-04-28 15:42:54 +05:30
Alexey Shekhirin
6715a093f1 Merge branch 'main' into bal-devnet-4 2026-04-28 11:11:07 +01:00
Soubhik Singha Mahapatra
d5bff1d478 Merge branch 'paradigmxyz:bal-devnet-4' into bal-devnet-4 2026-04-28 15:38:51 +05:30
Alexey Shekhirin
af6d20b5ea ci(hive): tag Reth image correctly and update fixtures (#23765) 2026-04-28 09:59:55 +00:00
Sergei Shulepov
64cf412aaf chore(engine): disable BAL parallel execution by default (#23764) 2026-04-28 09:27:19 +00:00
Matthias Seitz
5b10e03c5c perf(engine): spawn BAL hashed state before storage prefetch (#23761) 2026-04-28 08:39:55 +00:00
rakita
20141a2ea0 fmt a second try 2026-04-28 10:37:08 +02:00
Soubhik Singha Mahapatra
57962a1b95 Merge branch 'paradigmxyz:bal-devnet-4' into bal-devnet-4 2026-04-28 14:06:15 +05:30
rakita
60468fe256 fix test compilation: add missing fields in PrecompileOutput 2026-04-28 10:29:33 +02:00
rakita
ce3a171ce0 fmt 2026-04-28 10:22:46 +02:00
rakita
ce7e80ad33 chore(bal): bump bal-devnet-4 deps
Update revm, revm-inspectors, alloy-evm, and reth-core patches to
their latest bal-devnet-4 commits, and adjust call sites for the new
revm/alloy APIs (e.g. `ensure_intrinsic_gas` no longer takes
`block_gas_limit`).
2026-04-28 10:15:45 +02:00
Soubhik Singha Mahapatra
0c6a10d3fa Merge branch 'paradigmxyz:bal-devnet-4' into bal-devnet-4 2026-04-27 20:39:33 +05:30
Arsenii Kulikov
91d248e6fa feat: bound memory footpring of p2p messages (#23718) 2026-04-27 14:37:40 +00:00
Emma Jamieson-Hoare
d41a9a4078 Merge remote-tracking branch 'origin/main' into bal-devnet-4
Amp-Thread-ID: https://ampcode.com/threads/T-019dcf3c-4f22-724f-86bc-0968fe053595
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	crates/evm/evm/src/execute.rs
2026-04-27 16:02:19 +02:00
Emma Jamieson-Hoare
b984ddd275 chore(bal): latest devnet changes (#23737)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Soubhik Singha Mahapatra <160333583+Soubhik-10@users.noreply.github.com>
Co-authored-by: Ishika Choudhury <117741714+Rimeeeeee@users.noreply.github.com>
Co-authored-by: Brian Picciano <me@mediocregopher.com>
Co-authored-by: JOJO <wataxiwajojo@protonmail.com>
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
2026-04-27 15:45:26 +02:00
Brian Picciano
344037d04e perf(db): Pass ExecutedBlocks to OverlayBuilder, reduce reverts queried (#23657)
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-04-27 13:22:34 +00:00
Matthias Seitz
aca6261107 refactor(evm): return gas output from block builder (#23744) 2026-04-27 12:52:45 +00:00
Emma Jamieson-Hoare
b9c330e1a9 Merge branch 'main' into bal-devnet-4 2026-04-27 13:32:42 +01:00
Alexey Shekhirin
d4ca2e2687 ci: add Amsterdam Hive variant (#23736)
Co-authored-by: Soubhik Singha Mahapatra <soubhiksmp2004@gmail.com>
Co-authored-by: Soubhik Singha Mahapatra <160333583+Soubhik-10@users.noreply.github.com>
2026-04-27 12:00:04 +00:00
Alexey Shekhirin
e5e0abb47e perf(docker): add platform-specific RUSTFLAGS to Dockerfile (#23738) 2026-04-27 09:12:25 +00:00
joshieDo
cd10e6b47c feat(engine-api): squash-merge BAL cache from feat/bal-cache
Adds in-memory BalCache (HashMap + BTreeMap) with LRU eviction,
reorg handling, metrics, and engine_getBALsByHashV1/getBALsByRangeV1
RPC handlers.

Amp-Thread-ID: https://ampcode.com/threads/T-019dcde9-b8a4-725a-a510-a4474bc7059f
Co-authored-by: Amp <amp@ampcode.com>
2026-04-27 09:31:27 +01:00
JOJO
225e3ae238 fix(trie): account for heap-allocated blinded hashes in SparseNode::memory_size (#23726) 2026-04-27 07:50:35 +00:00
Brian Picciano
345fbbbfdb perf(trie): skip DB seek on exact overlay hits (#23559)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
2026-04-27 07:28:48 +00:00
Alexey Shekhirin
db17c899c3 fix(storage): reth db migrate-v2 for pruned nodes (#23716) 2026-04-27 07:14:03 +00:00
122 changed files with 8229 additions and 904 deletions

View File

@@ -1,6 +1,23 @@
#!/usr/bin/env bash
set -eo pipefail
fixture_variant="${1:-osaka}"
case "${fixture_variant}" in
amsterdam)
eels_fixtures="https://github.com/ethereum/execution-spec-tests/releases/download/bal@v6.0.0/fixtures_bal.tar.gz"
eels_branch="devnets/snøbal/4"
;;
osaka)
eels_fixtures="https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz"
eels_branch="forks/osaka"
;;
*)
echo "unknown hive fixture variant: ${fixture_variant}"
exit 1
;;
esac
# Create the hive_assets directory
mkdir hive_assets/
@@ -12,12 +29,12 @@ go build .
# Run each hive command in the background for each simulator and wait
echo "Building images"
./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="${eels_fixtures}" \
--sim.buildarg branch="${eels_branch}" \
--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="${eels_fixtures}" \
--sim.buildarg branch="${eels_branch}" \
--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

@@ -13,15 +13,15 @@ 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: []
sync: [ ]
sync: []
engine-auth: [ ]
engine-auth: []
# EIP-7610 related tests (Revert creation in case of non-empty storage):
#
@@ -99,6 +99,40 @@ 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
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Prague-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Shanghai-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Paris-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Cancun-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Osaka-blockchain_test_engine_from_state_test]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_engine_from_state_test-sstore-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_engine_from_state_test-empty-initcode]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_engine_from_state_test-initcode-with-deploy]-reth
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_engine_from_state_test-empty-initcode]-reth
# Blob limit tests:
#
@@ -106,6 +140,21 @@ eels/consume-engine:
# this test inserts a chain via chain.rlp where the last block is invalid, but expects import to stop there, this doesn't work properly with our pipeline import approach hence the import fails when the invalid block is detected.
#. In other words, if this test fails, this means we're correctly rejecting the block.
#. The same test exists in the consume-engine simulator where it is passing as expected
- 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
eels/consume-rlp:
- tests/prague/eip7702_set_code_tx/test_set_code_txs.py::test_set_code_to_non_empty_storage[fork_Prague-blockchain_test-zero_nonce]-reth
- tests/prague/eip7251_consolidations/test_modified_consolidation_contract.py::test_system_contract_errors[fork_Prague-blockchain_test_engine-system_contract_reaches_gas_limit-system_contract_0x0000bbddc7ce488642fb579f8b00f3a590007251]-reth
@@ -193,3 +242,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

@@ -5,6 +5,12 @@ cd hivetests/
sim="${1}"
limit="${2}"
fixture_variant="${3:-}"
if [[ "${fixture_variant}" == "osaka" && "${sim}" == *"eels"* && "${limit}" == *"tests/amsterdam"* ]]; then
echo "osaka fixtures do not support amsterdam tests"
exit 1
fi
# Use lower parallelism for eels tests to avoid OOM-killing the runner
parallelism=16

View File

@@ -6,6 +6,9 @@ on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
pull_request:
branches:
- "**"
env:
CARGO_TERM_COLOR: always
@@ -23,9 +26,16 @@ jobs:
secrets: inherit
prepare-hive:
if: github.repository == 'paradigmxyz/reth'
if: github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth'
timeout-minutes: 45
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
strategy:
fail-fast: false
matrix:
variant:
- amsterdam
- osaka
name: Prepare Hive - ${{ matrix.variant == 'amsterdam' && 'Amsterdam' || 'Osaka' }}
steps:
- uses: actions/checkout@v6
- name: Checkout hive tests
@@ -48,11 +58,11 @@ jobs:
uses: actions/cache@v5
with:
path: ./hive_assets
key: hive-assets-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/scripts/hive/build_simulators.sh') }}
key: hive-assets-${{ matrix.variant }}-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/scripts/hive/build_simulators.sh') }}
- name: Build hive assets
if: steps.cache-hive.outputs.cache-hit != 'true'
run: .github/scripts/hive/build_simulators.sh
run: .github/scripts/hive/build_simulators.sh ${{ matrix.variant }}
- name: Load cached Docker images
if: steps.cache-hive.outputs.cache-hit == 'true'
@@ -70,9 +80,186 @@ jobs:
- name: Upload hive assets
uses: actions/upload-artifact@v7
with:
name: hive_assets
name: hive_assets_${{ matrix.variant }}
path: ./hive_assets
test:
test-amsterdam:
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
# ethereum/rpc to be deprecated:
# https://github.com/ethereum/hive/pull/1117
scenario:
- sim: smoke/genesis
- sim: smoke/network
- sim: ethereum/sync
- sim: devp2p
limit: discv4
# started failing after https://github.com/ethereum/go-ethereum/pull/31843, no
# action on our side, remove from here when we get unexpected passes on these tests
# - sim: devp2p
# limit: eth
# include:
# - MaliciousHandshake
# # failures tracked in https://github.com/paradigmxyz/reth/issues/14825
# - Status
# - GetBlockHeaders
# - ZeroRequestID
# - GetBlockBodies
# - Transaction
# - NewPooledTxs
- sim: devp2p
limit: discv5
include:
# failures tracked at https://github.com/paradigmxyz/reth/issues/14825
- PingLargeRequestID
- sim: ethereum/engine
limit: engine-exchange-capabilities
- sim: ethereum/engine
limit: engine-withdrawals
- sim: ethereum/engine
limit: engine-auth
- sim: ethereum/engine
limit: engine-api
- sim: ethereum/engine
limit: cancun
# eth_ rpc methods
- 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_
# consume-engine
- sim: ethereum/eels/consume-engine
limit: .*tests/amsterdam.*
- sim: ethereum/eels/consume-engine
limit: .*tests/osaka.*
- sim: ethereum/eels/consume-engine
limit: .*tests/prague.*
- sim: ethereum/eels/consume-engine
limit: .*tests/cancun.*
- sim: ethereum/eels/consume-engine
limit: .*tests/shanghai.*
- sim: ethereum/eels/consume-engine
limit: .*tests/berlin.*
- sim: ethereum/eels/consume-engine
limit: .*tests/istanbul.*
- sim: ethereum/eels/consume-engine
limit: .*tests/homestead.*
- sim: ethereum/eels/consume-engine
limit: .*tests/frontier.*
- sim: ethereum/eels/consume-engine
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
limit: .*tests/prague.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/cancun.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/shanghai.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/berlin.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/istanbul.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/homestead.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/frontier.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/paris.*
needs:
- build-reth
- prepare-hive
name: Hive-Amsterdam / ${{ 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-oss' || github.repository == 'paradigmxyz/reth') && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
permissions:
issues: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Download hive assets
uses: actions/download-artifact@v8
with:
name: hive_assets_amsterdam
path: /tmp
- name: Download reth image
uses: actions/download-artifact@v8
with:
name: reth
path: /tmp
- name: Load Docker images
run: .github/scripts/hive/load_images.sh
- name: Move hive binary
run: |
mv /tmp/hive /usr/local/bin
chmod +x /usr/local/bin/hive
- name: Checkout hive tests
uses: actions/checkout@v6
with:
repository: ethereum/hive
ref: master
path: hivetests
- name: Run simulator
run: |
LIMIT="${{ matrix.scenario.limit }}"
TESTS="${{ join(matrix.scenario.include, '|') }}"
if [ -n "$LIMIT" ] && [ -n "$TESTS" ]; then
FILTER="$LIMIT/$TESTS"
elif [ -n "$LIMIT" ]; then
FILTER="$LIMIT"
elif [ -n "$TESTS" ]; then
FILTER="/$TESTS"
else
FILTER="/"
fi
echo "filter: $FILTER"
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER" "amsterdam"
- name: Parse hive output
run: |
find hivetests/workspace/logs -type f -name "*.json" ! -name "hive.json" | xargs -I {} python .github/scripts/hive/parse.py {} --exclusion .github/scripts/hive/expected_failures.yaml --ignored .github/scripts/hive/ignored_tests.yaml
- name: Print simulator output
if: ${{ failure() }}
run: |
cat hivetests/workspace/logs/*simulator*.log
- name: Print reth client logs
if: ${{ failure() }}
run: |
cat hivetests/workspace/logs/reth/client-*.log
test-osaka:
timeout-minutes: 120
strategy:
fail-fast: false
@@ -178,9 +365,9 @@ jobs:
needs:
- build-reth
- prepare-hive
name: ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
name: Hive-Osaka / ${{ 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' }}
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
permissions:
issues: write
steps:
@@ -191,7 +378,7 @@ jobs:
- name: Download hive assets
uses: actions/download-artifact@v8
with:
name: hive_assets
name: hive_assets_osaka
path: /tmp
- name: Download reth image
@@ -229,7 +416,7 @@ jobs:
FILTER="/"
fi
echo "filter: $FILTER"
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER"
.github/scripts/hive/run_simulator.sh "${{ matrix.scenario.sim }}" "$FILTER" "osaka"
- name: Parse hive output
run: |
@@ -245,7 +432,9 @@ jobs:
run: |
cat hivetests/workspace/logs/reth/client-*.log
notify-on-error:
needs: test
needs:
- test-amsterdam
- test-osaka
if: failure()
runs-on: ubuntu-latest
steps:

109
Cargo.lock generated
View File

@@ -290,8 +290,8 @@ dependencies = [
[[package]]
name = "alloy-evm"
version = "0.33.2"
source = "git+https://github.com/alloy-rs/evm?rev=da7633f6bc9554f5a6e60773ef21b8e9d6e0cca6#da7633f6bc9554f5a6e60773ef21b8e9d6e0cca6"
version = "0.34.0"
source = "git+https://github.com/alloy-rs/evm?branch=bal-devnet-4#aedf0f4a621ba3e63f0db6a570bdc9509296d1a6"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -7678,7 +7678,7 @@ dependencies = [
[[package]]
name = "reth-codecs"
version = "0.3.1"
source = "git+https://github.com/paradigmxyz/reth-core?rev=c763480b9fa51957fbdb69b7caead5dfc4e3752c#c763480b9fa51957fbdb69b7caead5dfc4e3752c"
source = "git+https://github.com/paradigmxyz/reth-core?rev=8612239c4f3dda83cc389f577b9eb04f10ebf81d#8612239c4f3dda83cc389f577b9eb04f10ebf81d"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -7698,7 +7698,7 @@ dependencies = [
[[package]]
name = "reth-codecs-derive"
version = "0.3.1"
source = "git+https://github.com/paradigmxyz/reth-core?rev=c763480b9fa51957fbdb69b7caead5dfc4e3752c#c763480b9fa51957fbdb69b7caead5dfc4e3752c"
source = "git+https://github.com/paradigmxyz/reth-core?rev=8612239c4f3dda83cc389f577b9eb04f10ebf81d#8612239c4f3dda83cc389f577b9eb04f10ebf81d"
dependencies = [
"proc-macro2",
"quote",
@@ -7728,6 +7728,7 @@ name = "reth-consensus"
version = "2.1.0"
dependencies = [
"alloy-consensus",
"alloy-eip7928",
"alloy-primitives",
"auto_impl",
"reth-execution-types",
@@ -8025,6 +8026,7 @@ dependencies = [
"reth-config",
"reth-consensus",
"reth-db",
"reth-db-api",
"reth-db-common",
"reth-engine-local",
"reth-engine-primitives",
@@ -8046,9 +8048,12 @@ dependencies = [
"reth-rpc-eth-api",
"reth-rpc-server-types",
"reth-stages-types",
"reth-storage-api",
"reth-tasks",
"reth-tokio-util",
"reth-tracing",
"reth-trie",
"reth-trie-db",
"revm",
"serde_json",
"tempfile",
@@ -8131,6 +8136,37 @@ dependencies = [
"tokio",
]
[[package]]
name = "reth-engine-snap"
version = "2.1.0"
dependencies = [
"alloy-consensus",
"alloy-eip7928",
"alloy-eips",
"alloy-primitives",
"alloy-rlp",
"alloy-trie",
"futures",
"reth-config",
"reth-consensus",
"reth-db-api",
"reth-downloaders",
"reth-eth-wire-types",
"reth-network-p2p",
"reth-primitives-traits",
"reth-provider",
"reth-stages",
"reth-stages-api",
"reth-stages-types",
"reth-storage-api",
"reth-tasks",
"reth-trie",
"reth-trie-db",
"thiserror 2.0.18",
"tokio",
"tracing",
]
[[package]]
name = "reth-engine-tree"
version = "2.1.0"
@@ -8163,6 +8199,7 @@ dependencies = [
"reth-db-common",
"reth-e2e-test-utils",
"reth-engine-primitives",
"reth-engine-snap",
"reth-errors",
"reth-ethereum-consensus",
"reth-ethereum-engine-primitives",
@@ -8357,6 +8394,7 @@ dependencies = [
"alloy-hardforks 0.4.7",
"alloy-primitives",
"alloy-rlp",
"alloy-trie",
"arbitrary",
"bytes",
"derive_more",
@@ -9058,6 +9096,7 @@ dependencies = [
"reth-downloaders",
"reth-engine-local",
"reth-engine-primitives",
"reth-engine-snap",
"reth-engine-tree",
"reth-engine-util",
"reth-ethereum-engine-primitives",
@@ -9087,6 +9126,7 @@ dependencies = [
"reth-rpc-layer",
"reth-stages",
"reth-static-file",
"reth-storage-api",
"reth-tasks",
"reth-tokio-util",
"reth-tracing",
@@ -9178,10 +9218,12 @@ dependencies = [
"jsonrpsee-core",
"rand 0.9.4",
"reth-chainspec",
"reth-config",
"reth-db",
"reth-e2e-test-utils",
"reth-engine-local",
"reth-engine-primitives",
"reth-engine-snap",
"reth-ethereum-consensus",
"reth-ethereum-engine-primitives",
"reth-ethereum-payload-builder",
@@ -9196,6 +9238,7 @@ dependencies = [
"reth-payload-primitives",
"reth-primitives-traits",
"reth-provider",
"reth-prune-types",
"reth-revm",
"reth-rpc",
"reth-rpc-api",
@@ -9203,7 +9246,9 @@ dependencies = [
"reth-rpc-eth-api",
"reth-rpc-eth-types",
"reth-rpc-server-types",
"reth-stages",
"reth-stages-types",
"reth-static-file",
"reth-tasks",
"reth-testing-utils",
"reth-tracing",
@@ -9382,7 +9427,7 @@ dependencies = [
[[package]]
name = "reth-primitives-traits"
version = "0.3.1"
source = "git+https://github.com/paradigmxyz/reth-core?rev=c763480b9fa51957fbdb69b7caead5dfc4e3752c#c763480b9fa51957fbdb69b7caead5dfc4e3752c"
source = "git+https://github.com/paradigmxyz/reth-core?rev=8612239c4f3dda83cc389f577b9eb04f10ebf81d#8612239c4f3dda83cc389f577b9eb04f10ebf81d"
dependencies = [
"alloy-consensus",
"alloy-eips",
@@ -9775,6 +9820,7 @@ dependencies = [
"jsonrpsee-core",
"jsonrpsee-types",
"metrics",
"parking_lot",
"reth-chainspec",
"reth-engine-primitives",
"reth-ethereum-engine-primitives",
@@ -9789,6 +9835,7 @@ dependencies = [
"reth-provider",
"reth-rpc-api",
"reth-storage-api",
"reth-storage-errors",
"reth-tasks",
"reth-testing-utils",
"reth-transaction-pool",
@@ -9926,7 +9973,7 @@ dependencies = [
[[package]]
name = "reth-rpc-traits"
version = "0.3.1"
source = "git+https://github.com/paradigmxyz/reth-core?rev=c763480b9fa51957fbdb69b7caead5dfc4e3752c#c763480b9fa51957fbdb69b7caead5dfc4e3752c"
source = "git+https://github.com/paradigmxyz/reth-core?rev=8612239c4f3dda83cc389f577b9eb04f10ebf81d#8612239c4f3dda83cc389f577b9eb04f10ebf81d"
dependencies = [
"alloy-consensus",
"alloy-network",
@@ -10403,6 +10450,8 @@ dependencies = [
"proptest-arbitrary-interop",
"rand 0.9.4",
"rayon",
"reth-chainspec",
"reth-ethereum-primitives",
"reth-execution-errors",
"reth-metrics",
"reth-primitives-traits",
@@ -10454,15 +10503,15 @@ dependencies = [
[[package]]
name = "reth-zstd-compressors"
version = "0.3.1"
source = "git+https://github.com/paradigmxyz/reth-core?rev=c763480b9fa51957fbdb69b7caead5dfc4e3752c#c763480b9fa51957fbdb69b7caead5dfc4e3752c"
source = "git+https://github.com/paradigmxyz/reth-core?rev=8612239c4f3dda83cc389f577b9eb04f10ebf81d#8612239c4f3dda83cc389f577b9eb04f10ebf81d"
dependencies = [
"zstd",
]
[[package]]
name = "revm"
version = "37.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
version = "38.0.0"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"revm-bytecode",
"revm-context",
@@ -10480,7 +10529,7 @@ dependencies = [
[[package]]
name = "revm-bytecode"
version = "10.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"bitvec",
"phf",
@@ -10490,8 +10539,8 @@ dependencies = [
[[package]]
name = "revm-context"
version = "16.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
version = "16.0.1"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"bitvec",
"cfg-if",
@@ -10506,8 +10555,8 @@ dependencies = [
[[package]]
name = "revm-context-interface"
version = "17.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
version = "17.0.1"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"alloy-eip2930",
"alloy-eip7702",
@@ -10521,8 +10570,8 @@ dependencies = [
[[package]]
name = "revm-database"
version = "13.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
version = "13.0.1"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"alloy-eips",
"revm-bytecode",
@@ -10534,8 +10583,8 @@ dependencies = [
[[package]]
name = "revm-database-interface"
version = "11.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
version = "11.0.1"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"auto_impl",
"either",
@@ -10547,8 +10596,8 @@ dependencies = [
[[package]]
name = "revm-handler"
version = "18.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
version = "18.1.0"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"auto_impl",
"derive-where",
@@ -10565,8 +10614,8 @@ dependencies = [
[[package]]
name = "revm-inspector"
version = "18.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
version = "19.0.0"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"auto_impl",
"either",
@@ -10583,7 +10632,7 @@ dependencies = [
[[package]]
name = "revm-inspectors"
version = "0.39.0"
source = "git+https://github.com/paradigmxyz/revm-inspectors?rev=a2c7a41977b468d016a339f560acb76e002766f3#a2c7a41977b468d016a339f560acb76e002766f3"
source = "git+https://github.com/paradigmxyz/revm-inspectors?rev=5eebb56819ee6bec5bfbc69a415276ee1a784fec#5eebb56819ee6bec5bfbc69a415276ee1a784fec"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -10601,8 +10650,8 @@ dependencies = [
[[package]]
name = "revm-interpreter"
version = "35.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
version = "35.0.1"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"revm-bytecode",
"revm-context-interface",
@@ -10613,8 +10662,8 @@ dependencies = [
[[package]]
name = "revm-precompile"
version = "33.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
version = "34.0.0"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"ark-bls12-381",
"ark-bn254",
@@ -10639,7 +10688,7 @@ dependencies = [
[[package]]
name = "revm-primitives"
version = "23.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"alloy-primitives",
"once_cell",
@@ -10648,8 +10697,8 @@ dependencies = [
[[package]]
name = "revm-state"
version = "11.0.0"
source = "git+https://github.com/bluealloy/revm?rev=fe2549d85fb9e201e7b629f8b47bcca46d49aa1d#fe2549d85fb9e201e7b629f8b47bcca46d49aa1d"
version = "11.0.1"
source = "git+https://github.com/bluealloy/revm?rev=3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be#3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be"
dependencies = [
"alloy-eip7928",
"bitflags 2.11.1",

View File

@@ -29,6 +29,7 @@ members = [
"crates/engine/primitives/",
"crates/engine/execution-cache/",
"crates/engine/tree/",
"crates/engine/snap/",
"crates/engine/util/",
"crates/era",
"crates/era-downloader",
@@ -345,6 +346,7 @@ 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-snap = { path = "crates/engine/snap" }
reth-engine-tree = { path = "crates/engine/tree" }
reth-engine-util = { path = "crates/engine/util" }
reth-era = { path = "crates/era" }
@@ -433,13 +435,13 @@ reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
reth-zstd-compressors = { version = "0.3.1", default-features = false }
# revm
revm = { version = "=37.0.0", default-features = false }
revm = { version = "=38.0.0", default-features = false }
revm-bytecode = { version = "=10.0.0", default-features = false }
revm-database = { version = "=13.0.0", default-features = false }
revm-state = { version = "=11.0.0", default-features = false }
revm-database = { version = "=13.0.1", default-features = false }
revm-state = { version = "=11.0.1", default-features = false }
revm-primitives = { version = "=23.0.0", default-features = false }
revm-interpreter = { version = "=35.0.0", default-features = false }
revm-database-interface = { version = "=11.0.0", default-features = false }
revm-interpreter = { version = "=35.0.1", default-features = false }
revm-database-interface = { version = "=11.0.1", default-features = false }
revm-inspectors = "=0.39.0"
# eth
@@ -450,7 +452,7 @@ alloy-sol-types = { version = "1.5.6", default-features = false }
alloy-chains = { version = "0.2.33", default-features = false }
alloy-eip2124 = { version = "0.2.0", default-features = false }
alloy-eip7928 = { version = "0.3.4", default-features = false }
alloy-evm = { version = "0.33.0", default-features = false }
alloy-evm = { version = "0.34.0", default-features = false }
alloy-rlp = { version = "0.3.13", default-features = false, features = ["core-net"] }
alloy-trie = { version = "0.9.4", default-features = false }
@@ -702,22 +704,22 @@ vergen-git2 = "9.1.0"
ipnet = "2.11"
[patch.crates-io]
revm = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-context = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-database = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-state = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "a2c7a41977b468d016a339f560acb76e002766f3" }
alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "da7633f6bc9554f5a6e60773ef21b8e9d6e0cca6" }
reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
revm = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-context = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-database = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-state = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "5eebb56819ee6bec5bfbc69a415276ee1a784fec" }
alloy-evm = { git = "https://github.com/alloy-rs/evm", branch = "bal-devnet-4" }
reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }

View File

@@ -33,8 +33,17 @@ ENV FEATURES=$FEATURES
RUN cargo chef cook --profile $BUILD_PROFILE --features "$FEATURES" --recipe-path recipe.json
# Build application
# Platform-specific RUSTFLAGS: amd64 uses x86-64-v3 (Haswell+) with pclmulqdq for rocksdb
#
# TARGETPLATFORM is set by BuildKit: https://docs.docker.com/reference/dockerfile#automatic-platform-args-in-the-global-scope
ARG TARGETPLATFORM
COPY --exclude=dist . .
RUN cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin reth
RUN if [ -n "$RUSTFLAGS" ]; then \
export RUSTFLAGS="$RUSTFLAGS"; \
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
export RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"; \
fi && \
cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin reth
# ARG is not resolved in COPY so we have to hack around it by copying the
# binary to a temporary location

View File

@@ -11,7 +11,7 @@ use alloy_eips::eip7685::Requests;
use alloy_evm::{
block::{
BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory,
BlockExecutorFor, ExecutableTx, GasOutput, OnStateHook, StateChangeSource, StateDB,
ExecutableTx, GasOutput, OnStateHook, StateChangeSource, StateDB,
},
eth::{EthBlockExecutionCtx, EthBlockExecutor, EthEvmContext, EthTxResult},
precompiles::PrecompilesMap,
@@ -36,6 +36,7 @@ use tracing::{debug, trace};
// ---------------------------------------------------------------------------
/// Runtime state for segment boundary tracking.
#[derive(Clone)]
pub(crate) struct BbEvmPlan {
/// The segment boundaries and environments.
pub(crate) segments: Vec<BigBlockSegment>,
@@ -73,6 +74,10 @@ impl BbEvmPlan {
.filter(|(n, _)| *n >= min && *n < block_number)
.collect()
}
pub(crate) fn segment_index_for_tx(&self, tx_index: usize) -> usize {
self.segments.partition_point(|segment| segment.start_tx <= tx_index).saturating_sub(1)
}
}
impl std::fmt::Debug for BbEvmPlan {
@@ -97,6 +102,9 @@ impl std::fmt::Debug for BbEvmPlan {
/// segment boundaries without requiring additional trait bounds on `DB`.
pub(crate) type BlockHashSeeder<DB> = fn(&mut DB, &[(u64, B256)]);
/// Function pointer that reads the BAL index from the DB.
pub(crate) type BalIndexReader<DB> = fn(&DB) -> u64;
/// Block executor that wraps [`EthBlockExecutor`] and handles segment-boundary
/// changes for big-block execution.
///
@@ -108,7 +116,7 @@ pub(crate) type BlockHashSeeder<DB> = fn(&mut DB, &[(u64, B256)]);
/// 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>
pub struct BbBlockExecutor<'a, DB, I, P, Spec>
where
DB: Database,
{
@@ -131,6 +139,25 @@ where
/// Callback to reseed block hashes into the DB's cache at segment
/// boundaries. See [`BlockHashSeeder`].
block_hash_seeder: Option<BlockHashSeeder<DB>>,
/// Callback to read the BAL index from the DB.
bal_index_reader: Option<BalIndexReader<DB>>,
/// Whether the executor has selected its starting segment.
initialized: bool,
}
impl<DB, I, P, Spec> std::fmt::Debug for BbBlockExecutor<'_, DB, I, P, Spec>
where
DB: Database,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BbBlockExecutor")
.field("has_inner", &self.inner.is_some())
.field("plan", &self.plan)
.field("gas_used_offset", &self.gas_used_offset)
.field("blob_gas_used_offset", &self.blob_gas_used_offset)
.field("initialized", &self.initialized)
.finish_non_exhaustive()
}
}
impl<'a, DB, I, P, Spec> BbBlockExecutor<'a, DB, I, P, Spec>
@@ -156,6 +183,7 @@ where
receipt_builder: RethReceiptBuilder,
plan: Option<BbEvmPlan>,
block_hash_seeder: Option<BlockHashSeeder<DB>>,
bal_index_reader: Option<BalIndexReader<DB>>,
) -> Self {
let inner = EthBlockExecutor::new(evm, ctx, spec, receipt_builder);
Self {
@@ -166,9 +194,63 @@ where
blob_gas_used_offset: 0,
shared_hook: Arc::new(Mutex::new(None)),
block_hash_seeder,
bal_index_reader,
initialized: false,
}
}
fn initialize(&mut self) -> Result<(), BlockExecutionError> {
if self.initialized {
return Ok(());
}
let plan = match &self.plan {
Some(plan) => plan,
None => return Ok(()),
};
self.initialized = true;
let bal_index =
self.bal_index_reader.map(|reader| reader(self.inner().evm().db())).unwrap_or(0);
let segment_idx =
if bal_index == 0 { 0 } else { plan.segment_index_for_tx((bal_index - 1) as usize) };
let segment = &plan.segments[segment_idx];
// Swap the EVM's block_env and executor ctx to the selected segment's
// values so that 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.
let block_env = segment.evm_env.block_env.clone();
let block_number = block_env.number.saturating_to::<u64>();
let mut cfg_env = segment.evm_env.cfg_env.clone();
cfg_env.disable_base_fee = true;
let ctx = EthBlockExecutionCtx {
parent_hash: segment.ctx.parent_hash,
parent_beacon_block_root: segment.ctx.parent_beacon_block_root,
ommers: segment.ctx.ommers,
withdrawals: segment.ctx.withdrawals.clone(),
extra_data: segment.ctx.extra_data.clone(),
tx_count_hint: segment.ctx.tx_count_hint,
slot_number: segment.ctx.slot_number,
};
let inner = self.inner_mut();
let evm_ctx = inner.evm.ctx_mut();
evm_ctx.block = block_env;
evm_ctx.cfg = cfg_env;
inner.ctx = ctx;
self.reseed_block_hashes_for(block_number);
if bal_index > 0 {
self.plan = None;
}
Ok(())
}
/// Creates a forwarding `OnStateHook` that delegates to the shared hook.
fn forwarding_hook(&self) -> Option<Box<dyn OnStateHook>> {
let shared = self.shared_hook.clone();
@@ -349,35 +431,9 @@ where
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,
slot_number: seg0.ctx.slot_number,
};
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);
}
// The outer big-block header uses a synthetic block number, so start
// system calls must run against the selected real segment env.
self.initialize()?;
self.inner_mut().apply_pre_execution_changes()
}
@@ -385,15 +441,13 @@ where
&mut self,
tx: impl ExecutableTx<Self>,
) -> Result<Self::Result, BlockExecutionError> {
self.initialize()?;
self.maybe_apply_boundary()?;
self.inner_mut().execute_transaction_without_commit(tx)
}
fn commit_transaction(
&mut self,
output: Self::Result,
) -> Result<GasOutput, BlockExecutionError> {
let gas_used = self.inner_mut().commit_transaction(output)?;
fn commit_transaction(&mut self, output: Self::Result) -> GasOutput {
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
@@ -408,7 +462,7 @@ where
if let Some(plan) = &mut self.plan {
plan.tx_counter += 1;
}
Ok(gas_used)
gas_used
}
fn finish(
@@ -499,7 +553,7 @@ pub struct BbBlockExecutorFactory<Spec> {
receipt_builder: RethReceiptBuilder,
spec: Spec,
evm_factory: EthEvmFactory,
/// Staged plan consumed by the next [`BbBlockExecutor`].
/// Staged plan cloned into each [`BbBlockExecutor`].
pub(crate) staged_plan: Arc<Mutex<Option<BbEvmPlan>>>,
}
@@ -528,8 +582,12 @@ impl<Spec> BbBlockExecutorFactory<Spec> {
*self.staged_plan.lock().unwrap() = Some(plan);
}
fn take_plan(&self) -> Option<BbEvmPlan> {
self.staged_plan.lock().unwrap().take()
pub(crate) fn clear_staged_plan(&self) {
*self.staged_plan.lock().unwrap() = None;
}
fn peek_plan(&self) -> Option<BbEvmPlan> {
self.staged_plan.lock().unwrap().clone()
}
pub(crate) fn create_executor_with_seeder<'a, DB, I>(
@@ -537,14 +595,23 @@ impl<Spec> BbBlockExecutorFactory<Spec> {
evm: EthEvm<DB, I, PrecompilesMap>,
ctx: EthBlockExecutionCtx<'a>,
block_hash_seeder: Option<BlockHashSeeder<DB>>,
bal_index_reader: Option<BalIndexReader<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)
let plan = self.peek_plan();
BbBlockExecutor::new(
evm,
ctx,
&self.spec,
self.receipt_builder,
plan,
block_hash_seeder,
bal_index_reader,
)
}
}
@@ -557,6 +624,9 @@ where
type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
type Transaction = TransactionSigned;
type Receipt = Receipt;
type TxExecutionResult = EthTxResult<HaltReason, alloy_consensus::TxType>;
type Executor<'a, DB: StateDB, I: Inspector<EthEvmContext<DB>>> =
BbBlockExecutor<'a, DB, I, PrecompilesMap, &'a Spec>;
fn evm_factory(&self) -> &Self::EvmFactory {
&self.evm_factory
@@ -566,12 +636,12 @@ where
&'a self,
evm: EthEvm<DB, I, PrecompilesMap>,
ctx: EthBlockExecutionCtx<'a>,
) -> impl BlockExecutorFor<'a, Self, DB, I>
) -> Self::Executor<'a, DB, I>
where
DB: StateDB + 'a,
I: Inspector<EthEvmContext<DB>> + 'a,
DB: StateDB,
I: Inspector<EthEvmContext<DB>>,
{
let plan = self.take_plan();
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, None)
let plan = self.peek_plan();
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, None, None)
}
}

View File

@@ -8,7 +8,7 @@
pub(crate) use reth_engine_primitives::BigBlockData;
use crate::{
evm::{BbBlockExecutorFactory, BbEvmPlan},
evm::{BalIndexReader, BbBlockExecutorFactory, BbEvmPlan},
BigBlockMap,
};
use alloy_consensus::Header;
@@ -55,7 +55,7 @@ pub(crate) struct BigBlockSegment {
///
/// 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
/// and cloned when executors are 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`].
@@ -106,6 +106,10 @@ fn seed_state_block_hashes<DB>(state: &mut &mut revm::database::State<DB>, hashe
}
}
fn read_bal_index<DB>(state: &&mut revm::database::State<DB>) -> u64 {
state.bal_state.bal_index()
}
// ---------------------------------------------------------------------------
// ConfigureEvm
// ---------------------------------------------------------------------------
@@ -144,6 +148,12 @@ where
&self,
block: &'a SealedBlock<reth_ethereum_primitives::Block>,
) -> Result<EthBlockExecutionCtx<'a>, Self::Error> {
if let Some(plan) = self.plan_for_payload_hash(&block.hash()) {
self.executor_factory.stage_plan(plan);
} else {
self.executor_factory.clear_staged_plan();
}
self.inner.context_for_block(block)
}
@@ -159,7 +169,7 @@ where
&'a self,
evm: reth_evm::EvmFor<Self, &'a mut revm::database::State<DB>, I>,
ctx: EthBlockExecutionCtx<'a>,
) -> impl alloy_evm::block::BlockExecutorFor<
) -> alloy_evm::block::BlockExecutorFor<
'a,
Self::BlockExecutorFactory,
&'a mut revm::database::State<DB>,
@@ -169,15 +179,16 @@ 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.
let bal_index_reader: Option<BalIndexReader<&'a mut revm::database::State<DB>>> =
Some(read_bal_index::<DB>);
// Inject concrete function pointers that know the `State<DB>` type so
// the generic executor can reseed block hashes and read `bal_index`.
self.executor_factory.create_executor_with_seeder(
evm,
ctx,
Some(seed_state_block_hashes::<DB>),
bal_index_reader,
)
}
}
@@ -214,6 +225,7 @@ where
Ok(env)
} else {
self.executor_factory.clear_staged_plan();
self.inner.evm_env_for_payload(payload)
}
}
@@ -248,10 +260,12 @@ where
/// 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 Some(plan) = self.plan_for_payload_hash(payload_hash) else { return };
self.executor_factory.stage_plan(plan);
}
fn plan_for_payload_hash(&self, payload_hash: &B256) -> Option<BbEvmPlan> {
let bb = self.pending.lock().unwrap().remove(payload_hash)?;
let segments: Vec<_> = bb
.env_switches
@@ -287,6 +301,6 @@ where
plan.block_hashes_to_seed.sort_unstable_by_key(|(n, _)| *n);
self.executor_factory.stage_plan(plan);
Some(plan)
}
}

View File

@@ -29,7 +29,10 @@ use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
use reth_ethereum_primitives::Receipt;
use reth_primitives_traits::proofs;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, future::Future};
use std::{
collections::{HashMap, HashSet},
future::Future,
};
use tracing::{info, warn};
use crate::bench::helpers::fetch_block_access_list;
@@ -717,6 +720,17 @@ fn merge_account_changes(existing: &mut AccountChanges, incoming: AccountChanges
existing.balance_changes.extend(incoming.balance_changes);
existing.nonce_changes.extend(incoming.nonce_changes);
existing.code_changes.extend(incoming.code_changes);
// EIP-7928 invariant: a slot must appear in either storage_changes or storage_reads,
// not both. Per-block BALs respect this, but merging blocks can produce a slot
// that is read in one block and changed in another. Without this normalization,
// an empty read entry can shadow the real writes during BAL deserialization,
// making reads of that slot fall through to stale snapshot state.
let written: HashSet<_> =
existing.storage_changes.iter().map(|slot_changes| slot_changes.slot).collect();
existing.storage_reads.retain(|slot| !written.contains(slot));
let mut seen = HashSet::with_capacity(existing.storage_reads.len());
existing.storage_reads.retain(|slot| seen.insert(*slot));
}
fn merge_slot_changes(existing: &mut Vec<SlotChanges>, incoming: Vec<SlotChanges>) {
@@ -836,4 +850,54 @@ mod tests {
assert_eq!(other.address, Address::repeat_byte(0x22));
assert_eq!(other.storage_changes[0].changes[0].block_access_index, 3);
}
#[test]
fn merge_account_changes_normalizes_storage_reads_after_cross_block_merge() {
let address = Address::repeat_byte(0x33);
const A: U256 = U256::from_limbs([1, 0, 0, 0]);
const B: U256 = U256::from_limbs([2, 0, 0, 0]);
const C: U256 = U256::from_limbs([3, 0, 0, 0]);
const D: U256 = U256::from_limbs([4, 0, 0, 0]);
// Each AccountChanges value is valid on its own: storage slots only appear in
// either reads or changes. The invalid read/change overlap is introduced when
// these per-block BAL entries are merged for a standalone big block.
let mut existing = AccountChanges {
address,
storage_changes: vec![SlotChanges::new(A, vec![StorageChange::new(0, U256::from(10))])],
storage_reads: vec![B, C],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
};
// B is read before it is written by the incoming block, and A is written before
// it appears as a read in the incoming block. C is read in both blocks, so the
// merge should also dedupe it. D remains read-only.
let incoming = AccountChanges {
address,
storage_changes: vec![SlotChanges::new(B, vec![StorageChange::new(1, U256::from(20))])],
storage_reads: vec![A, C, D],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
};
merge_account_changes(&mut existing, incoming);
// Written slots remain represented by storage_changes, while storage_reads only
// keeps unique read-only slots in first-seen order.
assert_eq!(
existing
.storage_changes
.iter()
.map(|slot_changes| slot_changes.slot)
.collect::<Vec<_>>(),
vec![A, B]
);
assert_eq!(existing.storage_reads, vec![C, D]);
assert!(existing.storage_reads.iter().all(|read_slot| {
!existing.storage_changes.iter().any(|slot_changes| slot_changes.slot == *read_slot)
}));
}
}

View File

@@ -18,7 +18,7 @@ reth-errors.workspace = true
reth-execution-types.workspace = true
reth-metrics.workspace = true
reth-ethereum-primitives.workspace = true
reth-primitives-traits.workspace = true
reth-primitives-traits = { workspace = true, features = ["dashmap"] }
reth-storage-api.workspace = true
reth-trie.workspace = true

View File

@@ -4,26 +4,32 @@
//! lazily on first access. This allows execution to start before the trie overlay
//! is fully computed.
use crate::DeferredTrieData;
use crate::{EthPrimitives, ExecutedBlock};
use alloy_primitives::B256;
use reth_primitives_traits::{
dashmap::{self, DashMap},
AlloyBlockHeader, NodePrimitives,
};
use reth_trie::{updates::TrieUpdatesSorted, HashedPostStateSorted, TrieInputSorted};
use std::sync::{Arc, OnceLock};
use std::sync::Arc;
use tracing::{debug, trace};
/// Inputs captured for lazy overlay computation.
#[derive(Clone)]
struct LazyOverlayInputs {
/// The persisted ancestor hash (anchor) this overlay should be built on.
anchor_hash: B256,
/// Deferred trie data handles for all in-memory blocks (newest to oldest).
blocks: Vec<DeferredTrieData>,
struct LazyOverlayInputs<N: NodePrimitives = EthPrimitives> {
/// In-memory blocks from tip to anchor child.
///
/// Blocks must be provided in reverse chain order (newest to oldest).
blocks: Vec<ExecutedBlock<N>>,
}
/// Lazily computed trie overlay.
///
/// Captures the inputs needed to compute a [`TrieInputSorted`] and defers the actual
/// computation until first access. This is conceptually similar to [`DeferredTrieData`]
/// but for overlay computation.
/// computation until first access.
///
/// Blocks must be provided in reverse chain order (newest to oldest), so the first block is the
/// chain tip and the last block is the oldest in-memory block in the chain segment.
///
/// # Fast Path vs Slow Path
///
@@ -31,37 +37,41 @@ struct LazyOverlayInputs {
/// matches our expected anchor, we can reuse it directly (O(1)).
/// - **Slow path**: Otherwise, we merge all ancestor blocks' trie data into a new overlay.
#[derive(Clone)]
pub struct LazyOverlay {
/// Computed result, cached after first access.
inner: Arc<OnceLock<TrieInputSorted>>,
pub struct LazyOverlay<N: NodePrimitives = EthPrimitives> {
/// Computed results, cached by requested anchor hash.
inner: Arc<DashMap<B256, Arc<TrieInputSorted>>>,
/// Inputs for lazy computation.
inputs: LazyOverlayInputs,
inputs: LazyOverlayInputs<N>,
}
impl std::fmt::Debug for LazyOverlay {
impl<N: NodePrimitives> std::fmt::Debug for LazyOverlay<N> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LazyOverlay")
.field("anchor_hash", &self.inputs.anchor_hash)
.field(
"oldest_block_parent_hash",
&self.inputs.blocks.last().map(|block| block.recovered_block().parent_hash()),
)
.field("num_blocks", &self.inputs.blocks.len())
.field("computed", &self.inner.get().is_some())
.field("cached_anchors", &self.inner.len())
.finish()
}
}
impl LazyOverlay {
/// Create a new lazy overlay with the given anchor hash and block handles.
impl<N: NodePrimitives> LazyOverlay<N> {
/// Create a new lazy overlay from in-memory blocks.
///
/// # Arguments
///
/// * `anchor_hash` - The persisted ancestor hash this overlay is built on top of
/// * `blocks` - Deferred trie data handles for in-memory blocks (newest to oldest)
pub fn new(anchor_hash: B256, blocks: Vec<DeferredTrieData>) -> Self {
Self { inner: Arc::new(OnceLock::new()), inputs: LazyOverlayInputs { anchor_hash, blocks } }
}
/// * `blocks` - Executed blocks in reverse chain order (newest to oldest)
pub fn new(blocks: Vec<ExecutedBlock<N>>) -> Self {
debug_assert!(
blocks.windows(2).all(|window| {
window[0].recovered_block().parent_hash() == window[1].recovered_block().hash()
}),
"LazyOverlay blocks must be ordered newest to oldest along a single chain"
);
/// Returns the anchor hash this overlay is built on.
pub const fn anchor_hash(&self) -> B256 {
self.inputs.anchor_hash
Self { inner: Default::default(), inputs: LazyOverlayInputs { blocks } }
}
/// Returns the number of in-memory blocks this overlay covers.
@@ -69,43 +79,75 @@ impl LazyOverlay {
self.inputs.blocks.len()
}
/// Returns true if the overlay has already been computed.
pub fn is_computed(&self) -> bool {
self.inner.get().is_some()
/// Returns the oldest anchor hash this overlay can serve.
///
/// This is the parent hash of the oldest block in the stored newest-to-oldest chain segment.
pub fn anchor_hash(&self) -> Option<B256> {
self.inputs.blocks.last().map(|block| block.recovered_block().parent_hash())
}
/// Returns the computed trie input, computing it if necessary.
/// Returns true if there are no blocks in the overlay, or if one of the blocks has the given
/// hash as a parent hash.
pub fn has_anchor_hash(&self, hash: B256) -> bool {
self.inputs.blocks.is_empty() ||
self.inputs.blocks.iter().any(|b| b.recovered_block().parent_hash() == hash)
}
#[cfg(test)]
/// Returns true if the overlay has already been computed for the requested anchor.
pub fn is_computed(&self, anchor_hash: B256) -> bool {
self.inner.contains_key(&anchor_hash)
}
/// Returns the computed trie input for the requested anchor, computing it if necessary.
///
/// The first call triggers computation (which may block waiting for deferred data).
/// Subsequent calls return the cached result immediately.
pub fn get(&self) -> &TrieInputSorted {
self.inner.get_or_init(|| self.compute())
/// Subsequent calls for the same anchor return the cached result immediately.
pub fn get(&self, anchor_hash: B256) -> Arc<TrieInputSorted> {
match self.inner.entry(anchor_hash) {
dashmap::Entry::Occupied(entry) => Arc::clone(entry.get()),
dashmap::Entry::Vacant(entry) => {
let input = self.compute(anchor_hash);
entry.insert(Arc::clone(&input));
input
}
}
}
/// Returns the overlay as (nodes, state) tuple for use with `OverlayStateProviderFactory`.
pub fn as_overlay(&self) -> (Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>) {
let input = self.get();
pub fn as_overlay(
&self,
anchor_hash: B256,
) -> (Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>) {
let input = self.get(anchor_hash);
(Arc::clone(&input.nodes), Arc::clone(&input.state))
}
/// Compute the trie input overlay.
fn compute(&self) -> TrieInputSorted {
let anchor_hash = self.inputs.anchor_hash;
fn compute(&self, anchor_hash: B256) -> Arc<TrieInputSorted> {
let blocks = &self.inputs.blocks;
if blocks.is_empty() {
debug!(target: "chain_state::lazy_overlay", "No in-memory blocks, returning empty overlay");
return TrieInputSorted::default();
return Default::default()
}
let Some(last_index) =
blocks.iter().position(|block| block.recovered_block().parent_hash() == anchor_hash)
else {
panic!(
"LazyOverlay does not contain a block whose parent hash matches requested anchor {anchor_hash}"
);
};
let blocks = &blocks[..=last_index];
// Fast path: Check if tip block's overlay is ready and anchor matches.
// The tip block (first in list) has the cumulative overlay from all ancestors.
// The tip block (first in list) has the cumulative overlay from all ancestors up to the
// requested anchor.
if let Some(tip) = blocks.first() {
let data = tip.wait_cloned();
let data = tip.trie_data();
if let Some(anchored) = &data.anchored_trie_input {
if anchored.anchor_hash == anchor_hash {
trace!(target: "chain_state::lazy_overlay", %anchor_hash, "Reusing tip block's cached overlay (fast path)");
return (*anchored.trie_input).clone();
return Arc::clone(&anchored.trie_input);
}
debug!(
target: "chain_state::lazy_overlay",
@@ -116,23 +158,30 @@ impl LazyOverlay {
}
}
// Slow path: Merge all blocks' trie data into a new overlay.
debug!(target: "chain_state::lazy_overlay", num_blocks = blocks.len(), "Merging blocks (slow path)");
Self::merge_blocks(blocks)
// Slow path: Merge the prefix of blocks from the tip back to the requested anchor.
debug!(
target: "chain_state::lazy_overlay",
%anchor_hash,
num_blocks = blocks.len(),
"Merging blocks (slow path)"
);
Arc::new(Self::merge_blocks(blocks))
}
/// Merge all blocks' trie data into a single [`TrieInputSorted`].
///
/// Blocks are ordered newest to oldest.
fn merge_blocks(blocks: &[DeferredTrieData]) -> TrieInputSorted {
fn merge_blocks(blocks: &[ExecutedBlock<N>]) -> TrieInputSorted {
if blocks.is_empty() {
return TrieInputSorted::default();
}
let state =
HashedPostStateSorted::merge_batch(blocks.iter().map(|b| b.wait_cloned().hashed_state));
let nodes =
TrieUpdatesSorted::merge_batch(blocks.iter().map(|b| b.wait_cloned().trie_updates));
let state = HashedPostStateSorted::merge_batch(
blocks.iter().map(|block| block.trie_data().hashed_state),
);
let nodes = TrieUpdatesSorted::merge_batch(
blocks.iter().map(|block| block.trie_data().trie_updates),
);
TrieInputSorted { state, nodes, prefix_sets: Default::default() }
}
@@ -141,46 +190,138 @@ impl LazyOverlay {
#[cfg(test)]
mod tests {
use super::*;
use reth_trie::{updates::TrieUpdates, HashedPostState};
use crate::{test_utils::TestBlockBuilder, ComputedTrieData, EthPrimitives, ExecutedBlock};
use alloy_primitives::U256;
use reth_primitives_traits::Account;
use reth_trie::{updates::TrieUpdatesSorted, HashedPostState, HashedStorage};
use std::sync::Arc;
fn empty_deferred(anchor: B256) -> DeferredTrieData {
DeferredTrieData::pending(
Arc::new(HashedPostState::default()),
Arc::new(TrieUpdates::default()),
anchor,
Vec::new(),
fn with_unique_state(
block: &ExecutedBlock<EthPrimitives>,
id: u8,
) -> ExecutedBlock<EthPrimitives> {
let hashed_address = B256::with_last_byte(id);
let hashed_slot = B256::with_last_byte(id.saturating_add(32));
let hashed_state = HashedPostState::default()
.with_accounts([(hashed_address, Some(Account::default()))])
.with_storages([(
hashed_address,
HashedStorage::from_iter(false, [(hashed_slot, U256::from(id))]),
)])
.into_sorted();
ExecutedBlock::new(
Arc::clone(&block.recovered_block),
Arc::clone(&block.execution_output),
ComputedTrieData::without_trie_input(
Arc::new(hashed_state),
Arc::new(TrieUpdatesSorted::default()),
),
)
}
#[test]
fn empty_blocks_returns_default() {
let overlay = LazyOverlay::new(B256::ZERO, vec![]);
let result = overlay.get();
assert!(result.state.is_empty());
assert!(result.nodes.is_empty());
fn test_blocks() -> Vec<ExecutedBlock<EthPrimitives>> {
TestBlockBuilder::eth()
.get_executed_blocks(1..4)
.collect::<Vec<_>>()
.into_iter()
.rev()
.enumerate()
.map(|(index, block)| with_unique_state(&block, index as u8 + 1))
.collect()
}
#[test]
fn single_block_uses_data_directly() {
let anchor = B256::random();
let deferred = empty_deferred(anchor);
let overlay = LazyOverlay::new(anchor, vec![deferred]);
let block = TestBlockBuilder::eth().get_executed_block_with_number(1, B256::random());
let anchor_hash = block.recovered_block().parent_hash();
let overlay = LazyOverlay::new(vec![block]);
assert!(!overlay.is_computed());
let _ = overlay.get();
assert!(overlay.is_computed());
assert!(!overlay.is_computed(anchor_hash));
let _ = overlay.get(anchor_hash);
assert!(overlay.is_computed(anchor_hash));
}
#[test]
fn cached_after_first_access() {
let overlay = LazyOverlay::new(B256::ZERO, vec![]);
fn caches_results_per_anchor() {
let blocks = test_blocks();
let prefix_anchor = blocks[2].recovered_block().hash();
let full_anchor = blocks[2].recovered_block().parent_hash();
let overlay = LazyOverlay::new(blocks);
// First access computes
let _ = overlay.get();
assert!(overlay.is_computed());
let prefix = overlay.get(prefix_anchor);
let full = overlay.get(full_anchor);
// Second access uses cache
let _ = overlay.get();
assert!(overlay.is_computed());
assert!(overlay.is_computed(prefix_anchor));
assert!(overlay.is_computed(full_anchor));
assert!(!Arc::ptr_eq(&prefix, &full));
assert!(Arc::ptr_eq(&prefix, &overlay.get(prefix_anchor)));
assert!(Arc::ptr_eq(&full, &overlay.get(full_anchor)));
}
#[test]
fn requested_anchor_limits_the_merged_prefix() {
let blocks = test_blocks();
let prefix_anchor = blocks[2].recovered_block().hash();
let expected = LazyOverlay::merge_blocks(&blocks[..2]);
let overlay = LazyOverlay::new(blocks);
let actual = overlay.get(prefix_anchor);
assert_eq!(actual.nodes.as_ref(), expected.nodes.as_ref());
assert_eq!(actual.state.as_ref(), expected.state.as_ref());
}
#[test]
fn anchor_hash_returns_oldest_served_anchor() {
let blocks = test_blocks();
let expected_anchor = blocks.last().unwrap().recovered_block().parent_hash();
let overlay = LazyOverlay::new(blocks);
assert_eq!(overlay.anchor_hash(), Some(expected_anchor));
}
#[test]
fn reuses_tip_overlay_when_anchor_matches() {
let mut blocks = test_blocks();
let prefix_anchor = blocks[2].recovered_block().hash();
let tip_overlay = Arc::new(LazyOverlay::merge_blocks(&blocks[..2]));
let tip_data = blocks[0].trie_data();
blocks[0] = ExecutedBlock::new(
Arc::clone(&blocks[0].recovered_block),
Arc::clone(&blocks[0].execution_output),
ComputedTrieData::with_trie_input(
tip_data.hashed_state,
tip_data.trie_updates,
prefix_anchor,
Arc::clone(&tip_overlay),
),
);
let overlay = LazyOverlay::new(blocks);
let actual = overlay.get(prefix_anchor);
assert!(Arc::ptr_eq(&actual, &tip_overlay));
}
#[test]
#[should_panic(
expected = "LazyOverlay does not contain a block whose parent hash matches requested anchor"
)]
fn missing_anchor_panics() {
let blocks = test_blocks();
let missing_anchor = blocks[0].recovered_block().hash();
let overlay = LazyOverlay::new(blocks);
let _ = overlay.get(missing_anchor);
}
#[test]
#[should_panic(
expected = "LazyOverlay blocks must be ordered newest to oldest along a single chain"
)]
fn misordered_blocks_panic() {
let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
let _ = LazyOverlay::new(blocks);
}
}

View File

@@ -5,6 +5,7 @@
//! state), compacts MDBX, then runs the pipeline to rebuild them.
use crate::common::CliNodeTypes;
use alloy_primitives::Address;
use clap::Parser;
use reth_db::{
mdbx::{self, ffi},
@@ -132,8 +133,12 @@ impl Command {
.and_then(|cp| cp.block_number)
.map_or(0, |b| b + 1);
let mut writer =
sf_provider.get_writer(first_block, StaticFileSegment::AccountChangeSets)?;
// The writer always starts at the fixed range boundary (e.g. 2500000) which may be
// earlier than first_block (e.g. 2603897 from prune checkpoint).
let mut writer = sf_provider.latest_writer(StaticFileSegment::AccountChangeSets)?;
if first_block > 0 {
writer.ensure_at_block(first_block - 1)?;
}
let mut count = 0u64;
let mut walker = cursor.walk(Some(first_block))?.peekable();
@@ -174,11 +179,15 @@ impl Command {
.and_then(|cp| cp.block_number)
.map_or(0, |b| b + 1);
let mut writer =
sf_provider.get_writer(first_block, StaticFileSegment::StorageChangeSets)?;
// The writer always starts at the fixed range boundary (e.g. 2500000) which may be
// earlier than first_block (e.g. 2603897 from prune checkpoint).
let mut writer = sf_provider.latest_writer(StaticFileSegment::StorageChangeSets)?;
if first_block > 0 {
writer.ensure_at_block(first_block - 1)?;
}
let mut count = 0u64;
let mut walker = cursor.walk(Some(Default::default()))?.peekable();
let mut walker = cursor.walk(Some((first_block, Address::ZERO).into()))?.peekable();
for block in first_block..=tip {
let mut entries = Vec::new();
@@ -238,6 +247,18 @@ impl Command {
.map_or(0, |b| b + 1);
let first_block = prune_start.max(existing.map_or(0, |b| b + 1));
// The writer always starts at the fixed range boundary (e.g. 2500000) which may be
// earlier than first_block (e.g. 2603897 from prune checkpoint).
if first_block > 0 {
let mut writer = sf_provider.latest_writer(StaticFileSegment::Receipts)?;
writer.ensure_at_block(first_block - 1)?;
writer.commit()?;
}
let before = sf_provider
.get_highest_static_file_tx(StaticFileSegment::Receipts)
.map_or(0, |tx| tx + 1);
let block_range = first_block..=tip;
let segment = reth_static_file::segments::Receipts;
@@ -245,7 +266,11 @@ impl Command {
sf_provider.commit()?;
info!(target: "reth::cli", "Receipts migrated");
let after = sf_provider
.get_highest_static_file_tx(StaticFileSegment::Receipts)
.map_or(0, |tx| tx + 1);
let count = after - before;
info!(target: "reth::cli", count, "Receipts migrated");
Ok(())
}

View File

@@ -191,8 +191,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)
.wrap_err_with(|| {
format!(
"Failed to validate block {} {}",

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,9 @@ std = [
"reth-primitives-traits/std",
"alloy-primitives/std",
"alloy-consensus/std",
"reth-primitives-traits/std",
"alloy-eip7928/std",
"reth-execution-types/std",
"thiserror/std",
"alloy-eip7928/std",
]
test-utils = [
"reth-primitives-traits/test-utils",
]
test-utils = ["reth-primitives-traits/test-utils"]

View File

@@ -38,6 +38,7 @@ use alloc::{
vec::Vec,
};
use alloy_consensus::Header;
use alloy_eip7928::BlockAccessList;
use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256};
use core::{error::Error, fmt::Display};
@@ -85,6 +86,7 @@ pub trait FullConsensus<N: NodePrimitives>: Consensus<N::Block> {
block: &RecoveredBlock<N::Block>,
result: &BlockExecutionResult<N::Receipt>,
receipt_root_bloom: Option<ReceiptRootBloom>,
block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError>;
}
@@ -474,6 +476,12 @@ pub enum ConsensusError {
/// EIP-7825: Transaction gas limit exceeds maximum allowed
#[error(transparent)]
TransactionGasLimitTooHigh(Box<TxGasLimitTooHighErr>),
/// Error when an unexpected block access list cost is encountered.
#[error("block access list exceeds gas limit")]
BlockAccessListExceedsGasLimit,
/// Error when the block access list hash doesn't match the expected value.
#[error("block access list hash mismatch: {0}")]
BlockAccessListHashMismatch(GotExpectedBoxed<B256>),
/// Any additional consensus error, for example L2-specific errors.
#[error(transparent)]
Other(#[from] Arc<dyn Error + Send + Sync>),
@@ -519,6 +527,23 @@ impl ConsensusError {
}
}
/// Validates the block access list against the gas limit.
///
/// EIP-7925 specifies that the total cost of the block access list items must not exceed
/// the gas limit. Each item costs `ITEM_COST` gas.
pub fn validate_block_access_list_gas(
block_access_list: Option<&alloy_eip7928::BlockAccessList>,
gas_limit: u64,
) -> Result<(), ConsensusError> {
if let Some(bal) = block_access_list {
let bal_items = alloy_eip7928::total_bal_items(bal);
if bal_items > gas_limit / alloy_eip7928::ITEM_COST as u64 {
return Err(ConsensusError::BlockAccessListExceedsGasLimit)
}
}
Ok(())
}
impl From<InvalidTransactionError> for ConsensusError {
fn from(value: InvalidTransactionError) -> Self {
Self::InvalidTransaction(value)

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,7 @@ 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>,
) -> 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,7 @@ 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>,
) -> Result<(), ConsensusError> {
if self.fail_validation() {
Err(ConsensusError::BaseFeeMissing)

View File

@@ -14,8 +14,12 @@ workspace = true
reth-chainspec.workspace = true
reth-tracing.workspace = true
reth-db = { workspace = true, features = ["test-utils"] }
reth-db-api.workspace = true
reth-network-api.workspace = true
reth-network-p2p.workspace = true
reth-storage-api = { workspace = true, features = ["db-api"] }
reth-trie = { workspace = true, features = ["test-utils"] }
reth-trie-db.workspace = true
reth-rpc-server-types.workspace = true
reth-rpc-builder.workspace = true
reth-rpc-eth-api.workspace = true

View File

@@ -4,7 +4,7 @@ use alloy_rpc_types_engine::PayloadAttributes;
use node::NodeTestContext;
use reth_chainspec::ChainSpec;
use reth_db::{test_utils::TempDatabase, DatabaseEnv};
use reth_network_api::test_utils::PeersHandleProvider;
use reth_network_api::{test_utils::PeersHandleProvider, BlockDownloaderProvider};
use reth_node_builder::{
components::NodeComponentsBuilder,
rpc::{EngineValidatorAddOn, RethRpcAddOns},
@@ -34,6 +34,8 @@ pub mod setup_import;
/// Helper for network operations
mod network;
/// Snap sync utilities for E2E tests.
/// Helper for rpc operations
mod rpc;
@@ -153,7 +155,11 @@ where
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Components: NodeComponents<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Network: PeersHandleProvider,
Network: PeersHandleProvider
+ BlockDownloaderProvider<
Client: reth_network_p2p::snap::client::SnapClient
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient,
>,
>,
>,
AddOns: RethRpcAddOns<
@@ -175,7 +181,11 @@ impl<T> NodeBuilderHelper for T where
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Components: NodeComponents<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Network: PeersHandleProvider,
Network: PeersHandleProvider
+ BlockDownloaderProvider<
Client: reth_network_p2p::snap::client::SnapClient
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient,
>,
>,
>,
AddOns: RethRpcAddOns<

View File

@@ -8,6 +8,7 @@ use eyre::Ok;
use futures_util::Future;
use jsonrpsee::{core::client::ClientT, http_client::HttpClient};
use reth_chainspec::EthereumHardforks;
use reth_db_api::transaction::DbTx;
use reth_network_api::test_utils::PeersHandleProvider;
use reth_node_api::{Block, BlockBody, BlockTy, FullNodeComponents, PayloadTypes, PrimitivesTy};
use reth_node_builder::{rpc::RethRpcAddOns, FullNode, NodeTypes};
@@ -15,12 +16,13 @@ use reth_node_builder::{rpc::RethRpcAddOns, FullNode, NodeTypes};
use reth_payload_primitives::BuiltPayload;
use reth_provider::{
BlockReader, BlockReaderIdExt, CanonStateNotificationStream, CanonStateSubscriptions,
HeaderProvider, StageCheckpointReader,
DatabaseProviderFactory, HeaderProvider, StageCheckpointReader,
};
use reth_rpc_api::TestingBuildBlockRequestV1;
use reth_rpc_builder::auth::AuthServerHandle;
use reth_rpc_eth_api::helpers::{EthApiSpec, EthTransactions, TraceExt};
use reth_stages_types::StageId;
use reth_storage_api::DBProvider;
use std::pin::Pin;
use tokio_stream::StreamExt;
use url::Url;
@@ -312,7 +314,7 @@ where
self.inner
.add_ons_handle
.beacon_engine_handle
.new_payload(Payload::block_to_payload(payload.block().clone()))
.new_payload(Payload::built_payload_to_execution_data(&payload))
.await?;
Ok(block_hash)
@@ -365,4 +367,26 @@ where
client.request("testing_buildBlockV1", [request]).await?;
eyre::Ok(res)
}
/// Computes the current state root from the persisted `HashedAccounts` /
/// `HashedStorages` tables in MDBX. This reflects the latest block whose
/// hashed state has been committed to disk by the engine persistence layer.
///
/// Uses [`NoopTrieCursorFactory`] so the root is computed purely from the
/// hashed leaf data, without depending on the incremental trie tables.
pub async fn snap_state_root(&self) -> B256
where
Node::Provider: DatabaseProviderFactory,
<Node::Provider as DatabaseProviderFactory>::Provider: DBProvider,
<<Node::Provider as DatabaseProviderFactory>::Provider as DBProvider>::Tx: DbTx,
{
use reth_trie::{trie_cursor::noop::NoopTrieCursorFactory, StateRoot};
use reth_trie_db::DatabaseHashedCursorFactory;
let provider = self.inner.provider.database_provider_ro().expect("open ro provider");
let tx = provider.tx_ref();
StateRoot::new(NoopTrieCursorFactory::default(), DatabaseHashedCursorFactory::new(tx))
.root()
.unwrap_or(B256::ZERO)
}
}

View File

@@ -239,7 +239,7 @@ impl Default for TreeConfig {
share_execution_cache_with_payload_builder: false,
share_sparse_trie_with_payload_builder: false,
suppress_persistence_during_build: false,
disable_bal_parallel_execution: false,
disable_bal_parallel_execution: true,
disable_bal_parallel_state_root: false,
disable_bal_batch_io: false,
#[cfg(feature = "trie-debug")]
@@ -316,7 +316,7 @@ impl TreeConfig {
share_execution_cache_with_payload_builder,
share_sparse_trie_with_payload_builder,
suppress_persistence_during_build: false,
disable_bal_parallel_execution: false,
disable_bal_parallel_execution: true,
disable_bal_parallel_state_root: false,
disable_bal_batch_io: false,
#[cfg(feature = "trie-debug")]

View File

@@ -0,0 +1,65 @@
[package]
name = "reth-engine-snap"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
# reth
reth-db-api.workspace = true
reth-config.workspace = true
reth-consensus.workspace = true
reth-downloaders.workspace = true
reth-eth-wire-types.workspace = true
reth-network-p2p.workspace = true
reth-primitives-traits.workspace = true
reth-provider.workspace = true
reth-stages.workspace = true
reth-stages-api.workspace = true
reth-stages-types.workspace = true
reth-storage-api.workspace = true
reth-tasks = { workspace = true, features = ["rayon"] }
reth-trie.workspace = true
reth-trie-db.workspace = true
# alloy
alloy-consensus.workspace = true
alloy-eip7928 = { workspace = true, features = ["rlp"] }
alloy-eips.workspace = true
alloy-primitives.workspace = true
alloy-rlp.workspace = true
# async
tokio = { workspace = true, features = ["sync", "time"] }
# misc
futures.workspace = true
tracing.workspace = true
thiserror.workspace = true
[dev-dependencies]
alloy-primitives = { workspace = true, features = ["rand"] }
alloy-trie.workspace = true
[features]
default = []
test-utils = [
"reth-consensus/test-utils",
"reth-db-api/test-utils",
"reth-downloaders/test-utils",
"reth-network-p2p/test-utils",
"reth-primitives-traits/test-utils",
"reth-provider/test-utils",
"reth-stages/test-utils",
"reth-stages-api/test-utils",
"reth-stages-types/test-utils",
"reth-tasks/test-utils",
"reth-trie/test-utils",
"reth-trie-db/test-utils",
]

View File

@@ -0,0 +1,332 @@
//! BAL (Block Access List) diff application for snap sync.
//!
//! Converts raw `Vec<AccountChanges>` from a single block's BAL into
//! partial account diffs, storage writes, and bytecode entries that can
//! be merged with existing hashed state.
use alloy_consensus::constants::KECCAK_EMPTY;
use alloy_eip7928::AccountChanges;
use alloy_primitives::{keccak256, Bytes, B256, U256};
/// A partial diff for a single account extracted from one block's BAL.
///
/// Fields are `Some` only when the BAL contains at least one change for that
/// field. The caller must merge these with existing DB state before writing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BalAccountDiff {
/// `keccak256(address)`.
pub hashed_address: B256,
/// Final balance if `balance_changes` was non-empty.
pub balance: Option<U256>,
/// Final nonce if `nonce_changes` was non-empty.
pub nonce: Option<u64>,
/// Final bytecode hash if `code_changes` was non-empty.
/// Inner `None` means the code was cleared (empty code).
pub bytecode_hash: Option<Option<B256>>,
}
/// Storage entries extracted from one block's BAL.
///
/// Each entry is `(hashed_address, hashed_slot, final_value)`.
pub type BalStorageEntry = (B256, B256, U256);
/// Bytecode entries extracted from one block's BAL.
///
/// Each entry is `(code_hash, code_bytes)`.
pub type BalBytecodeEntry = (B256, Bytes);
/// Parsed state diffs from a single block's BAL.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BalStateDiff {
/// Per-account partial diffs (fields that changed in this block).
pub accounts: Vec<BalAccountDiff>,
/// `(hashed_address, hashed_slot, value)` triples for storage writes.
pub storage: Vec<BalStorageEntry>,
/// `(code_hash, code_bytes)` pairs for bytecode writes.
pub bytecodes: Vec<BalBytecodeEntry>,
}
/// Convert a list of [`AccountChanges`] (from one block's BAL) into partial
/// state diffs.
///
/// For each account, the final post-block value is the entry with the highest
/// transaction index (i.e. the last element, since entries are ordered).
/// Fields with empty change lists are left as `None` in [`BalAccountDiff`],
/// meaning they were not modified in this block.
pub fn bal_to_state_diff(changes: &[AccountChanges]) -> BalStateDiff {
let mut diff = BalStateDiff::default();
for ac in changes {
let hashed_address = keccak256(ac.address);
// Last balance change → final balance.
let balance = ac.balance_changes.last().map(|c| c.post_balance);
// Last nonce change → final nonce.
let nonce = ac.nonce_changes.last().map(|c| c.new_nonce);
// Last code change → final code hash + bytecodes entry.
let bytecode_hash = ac.code_changes.last().map(|c| {
if c.new_code.is_empty() {
None
} else {
let code_hash = keccak256(&c.new_code);
diff.bytecodes.push((code_hash, c.new_code.clone()));
Some(code_hash)
}
});
// Storage: for each slot, take the last change's value.
for slot_changes in &ac.storage_changes {
if let Some(last_change) = slot_changes.changes.last() {
let hashed_slot = keccak256(slot_changes.slot.to_be_bytes::<32>());
diff.storage.push((hashed_address, hashed_slot, last_change.new_value));
}
}
// Only emit an account diff if at least one field changed.
if balance.is_some() || nonce.is_some() || bytecode_hash.is_some() {
diff.accounts.push(BalAccountDiff { hashed_address, balance, nonce, bytecode_hash });
}
}
diff
}
/// Merge a [`BalAccountDiff`] with an existing [`Account`], returning the
/// updated account.
///
/// Fields that are `None` in the diff retain their existing values. If
/// `existing` is `None` (new account), absent fields default to zero / no code.
pub fn merge_account_diff(
diff: &BalAccountDiff,
existing: Option<&reth_primitives_traits::Account>,
) -> reth_primitives_traits::Account {
reth_primitives_traits::Account {
balance: diff.balance.unwrap_or_else(|| existing.map(|a| a.balance).unwrap_or(U256::ZERO)),
nonce: diff.nonce.unwrap_or_else(|| existing.map(|a| a.nonce).unwrap_or(0)),
bytecode_hash: match diff.bytecode_hash {
Some(hash) => {
// Explicit code change: Some(hash) for non-empty, None for cleared.
// Normalize: treat KECCAK_EMPTY as None (no code).
match hash {
Some(h) if h == KECCAK_EMPTY => None,
other => other,
}
}
None => {
// No code change in this block — keep existing.
existing.and_then(|a| a.bytecode_hash)
}
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_eip7928::{BalanceChange, CodeChange, NonceChange, SlotChanges, StorageChange};
use alloy_primitives::Address;
#[test]
fn all_fields_present() {
let addr = Address::from([0xaa; 20]);
let code = Bytes::from(vec![0x60, 0x00, 0x56]);
let code_hash = keccak256(&code);
let changes = vec![AccountChanges {
address: addr,
balance_changes: vec![
BalanceChange::new(0, U256::from(100)),
BalanceChange::new(1, U256::from(200)),
],
nonce_changes: vec![NonceChange::new(0, 1), NonceChange::new(1, 2)],
code_changes: vec![CodeChange::new(0, code.clone())],
storage_changes: vec![SlotChanges::new(
U256::from(1),
vec![StorageChange::new(0, U256::from(10)), StorageChange::new(1, U256::from(20))],
)],
storage_reads: vec![],
}];
let diff = bal_to_state_diff(&changes);
assert_eq!(diff.accounts.len(), 1);
let acct = &diff.accounts[0];
assert_eq!(acct.hashed_address, keccak256(addr));
assert_eq!(acct.balance, Some(U256::from(200)));
assert_eq!(acct.nonce, Some(2));
assert_eq!(acct.bytecode_hash, Some(Some(code_hash)));
assert_eq!(diff.storage.len(), 1);
let (ha, hs, val) = &diff.storage[0];
assert_eq!(*ha, keccak256(addr));
assert_eq!(*hs, keccak256(U256::from(1).to_be_bytes::<32>()));
assert_eq!(*val, U256::from(20));
assert_eq!(diff.bytecodes.len(), 1);
assert_eq!(diff.bytecodes[0], (code_hash, code));
}
#[test]
fn partial_fields_only_balance() {
let addr = Address::from([0xbb; 20]);
let changes = vec![AccountChanges {
address: addr,
balance_changes: vec![BalanceChange::new(0, U256::from(500))],
nonce_changes: vec![],
code_changes: vec![],
storage_changes: vec![],
storage_reads: vec![],
}];
let diff = bal_to_state_diff(&changes);
assert_eq!(diff.accounts.len(), 1);
let acct = &diff.accounts[0];
assert_eq!(acct.balance, Some(U256::from(500)));
assert_eq!(acct.nonce, None);
assert_eq!(acct.bytecode_hash, None);
assert!(diff.storage.is_empty());
assert!(diff.bytecodes.is_empty());
}
#[test]
fn last_entry_wins() {
let addr = Address::from([0xcc; 20]);
let changes = vec![AccountChanges {
address: addr,
balance_changes: vec![
BalanceChange::new(0, U256::from(10)),
BalanceChange::new(1, U256::from(20)),
BalanceChange::new(5, U256::from(99)),
],
nonce_changes: vec![NonceChange::new(0, 1), NonceChange::new(3, 7)],
code_changes: vec![],
storage_changes: vec![SlotChanges::new(
U256::from(42),
vec![
StorageChange::new(0, U256::from(100)),
StorageChange::new(2, U256::from(300)),
StorageChange::new(4, U256::from(999)),
],
)],
storage_reads: vec![],
}];
let diff = bal_to_state_diff(&changes);
assert_eq!(diff.accounts[0].balance, Some(U256::from(99)));
assert_eq!(diff.accounts[0].nonce, Some(7));
assert_eq!(diff.storage[0].2, U256::from(999));
}
#[test]
fn empty_changes_no_account_diff() {
let addr = Address::from([0xdd; 20]);
let changes = vec![AccountChanges {
address: addr,
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
storage_changes: vec![],
storage_reads: vec![U256::from(1)],
}];
let diff = bal_to_state_diff(&changes);
// No account, storage, or bytecode diffs — only reads.
assert!(diff.accounts.is_empty());
assert!(diff.storage.is_empty());
assert!(diff.bytecodes.is_empty());
}
#[test]
fn empty_code_clears_bytecode_hash() {
let addr = Address::from([0xee; 20]);
let changes = vec![AccountChanges {
address: addr,
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![CodeChange::new(0, Bytes::new())],
storage_changes: vec![],
storage_reads: vec![],
}];
let diff = bal_to_state_diff(&changes);
assert_eq!(diff.accounts.len(), 1);
// Empty code → bytecode_hash = Some(None) (cleared).
assert_eq!(diff.accounts[0].bytecode_hash, Some(None));
assert!(diff.bytecodes.is_empty());
}
#[test]
fn merge_with_existing_account() {
let existing = reth_primitives_traits::Account {
nonce: 5,
balance: U256::from(1000),
bytecode_hash: Some(B256::from([0xab; 32])),
};
let diff = BalAccountDiff {
hashed_address: B256::ZERO,
balance: Some(U256::from(2000)),
nonce: None,
bytecode_hash: None,
};
let merged = merge_account_diff(&diff, Some(&existing));
assert_eq!(merged.balance, U256::from(2000));
assert_eq!(merged.nonce, 5); // kept from existing
assert_eq!(merged.bytecode_hash, Some(B256::from([0xab; 32]))); // kept from existing
}
#[test]
fn merge_new_account_defaults() {
let diff = BalAccountDiff {
hashed_address: B256::ZERO,
balance: Some(U256::from(100)),
nonce: None,
bytecode_hash: None,
};
let merged = merge_account_diff(&diff, None);
assert_eq!(merged.balance, U256::from(100));
assert_eq!(merged.nonce, 0);
assert_eq!(merged.bytecode_hash, None);
}
#[test]
fn multiple_accounts() {
let changes = vec![
AccountChanges {
address: Address::from([0x01; 20]),
balance_changes: vec![BalanceChange::new(0, U256::from(100))],
nonce_changes: vec![NonceChange::new(0, 1)],
code_changes: vec![],
storage_changes: vec![],
storage_reads: vec![],
},
AccountChanges {
address: Address::from([0x02; 20]),
balance_changes: vec![BalanceChange::new(0, U256::from(200))],
nonce_changes: vec![],
code_changes: vec![],
storage_changes: vec![SlotChanges::new(
U256::from(5),
vec![StorageChange::new(0, U256::from(50))],
)],
storage_reads: vec![],
},
];
let diff = bal_to_state_diff(&changes);
assert_eq!(diff.accounts.len(), 2);
assert_eq!(diff.accounts[0].hashed_address, keccak256(Address::from([0x01; 20])));
assert_eq!(diff.accounts[1].hashed_address, keccak256(Address::from([0x02; 20])));
assert_eq!(diff.storage.len(), 1);
assert_eq!(diff.storage[0].0, keccak256(Address::from([0x02; 20])));
}
}

View File

@@ -0,0 +1,314 @@
//! Snap sync lifecycle controller.
//!
//! The controller owns Phase A header download and then spawns the engine-driven
//! [`SnapSyncOrchestrator`](crate::orchestrator::SnapSyncOrchestrator) for
//! Phase B state download and BAL catch-up.
use crate::{SnapSyncError, SnapSyncEvent, SnapSyncOutcome};
use alloy_consensus::BlockHeader;
use futures::FutureExt;
use reth_config::config::EtlConfig;
use reth_consensus::noop::NoopConsensus;
use reth_db_api::{
table::Value,
transaction::{DbTx, DbTxMut},
};
use reth_downloaders::headers::reverse_headers::ReverseHeadersDownloaderBuilder;
use reth_network_p2p::{headers::client::HeadersClient, snap::client::SnapClient};
use reth_primitives_traits::{FullBlockHeader, NodePrimitives};
use reth_provider::{
providers::StaticFileWriter, DatabaseProviderFactory, HeaderProvider,
StaticFileProviderFactory, StorageSettingsCache,
};
use reth_stages::stages::HeaderStage;
use reth_stages_api::{ExecInput, Stage, StageCheckpoint, StageExt, StageId};
use reth_storage_api::{
DBProvider, HeaderSyncGapProvider, NodePrimitivesProvider, StageCheckpointWriter, StateWriter,
};
use reth_tasks::Runtime;
use std::task::{Context, Poll};
use tokio::sync::{mpsc::UnboundedSender, oneshot};
/// Events emitted by [`SnapSyncController`].
#[derive(Debug)]
pub enum SnapSyncControlEvent {
/// Phase B started and engine events can now be forwarded through the sender.
Started(UnboundedSender<SnapSyncEvent>),
/// Snap sync finished.
Finished(Result<SnapSyncOutcome, SnapSyncError>),
/// A controller task was dropped or Phase A failed.
TaskDropped(String),
}
/// Snap sync lifecycle control surface.
pub trait SnapSyncControl: Send {
/// Returns `true` if snap sync is active.
fn is_active(&self) -> bool;
/// Starts snap sync toward the target block hash.
fn start(&mut self, target_hash: alloy_primitives::B256) -> bool;
/// Polls snap sync for its next lifecycle event.
fn poll(&mut self, cx: &mut Context<'_>) -> Poll<SnapSyncControlEvent>;
}
/// Header type used by snap sync for the given provider factory.
pub type SnapSyncHeader<F> =
<<<F as DatabaseProviderFactory>::ProviderRW as NodePrimitivesProvider>::Primitives as NodePrimitives>::BlockHeader;
/// A snap sync controller that manages the snap sync lifecycle.
///
/// Snap sync runs in two phases:
/// - **Phase A**: download headers 1..pivot via `HeaderStage` so that static files are populated
/// before any state download begins.
/// - **Phase B**: hand off to [`SnapSyncOrchestrator`](crate::orchestrator::SnapSyncOrchestrator)
/// for state download and BAL catch-up.
#[derive(Debug)]
pub struct SnapSyncController<C, F> {
client: C,
factory: F,
runtime: Runtime,
/// The target hash passed to [`Self::start`], kept until Phase A completes.
target_hash: Option<alloy_primitives::B256>,
/// State of the snap sync: `None` = idle, `Some` = running.
state: Option<SnapSyncState>,
}
/// Running state of snap sync.
#[derive(Debug)]
enum SnapSyncState {
/// Phase A: downloading headers via `HeaderStage`.
DownloadingHeaders { result_rx: oneshot::Receiver<Result<(), String>> },
/// Phase B: state download via orchestrator.
DownloadingState {
/// Sender for forwarding engine events to the orchestrator.
///
/// Kept alive so the orchestrator's receiver doesn't close prematurely.
#[expect(dead_code)]
events_tx: UnboundedSender<SnapSyncEvent>,
/// Receiver for the orchestrator result.
result_rx: oneshot::Receiver<Result<SnapSyncOutcome, SnapSyncError>>,
},
}
impl<C, F> SnapSyncController<C, F> {
/// Creates a new controller.
pub fn new(client: C, factory: F, runtime: Runtime) -> Self {
Self { client, factory, runtime, target_hash: None, state: None }
}
/// Returns `true` if snap sync is currently active.
pub const fn is_active(&self) -> bool {
self.state.is_some()
}
}
impl<C, F> SnapSyncController<C, F>
where
C: SnapClient + HeadersClient<Header = SnapSyncHeader<F>> + Clone + Send + Sync + 'static,
F: DatabaseProviderFactory
+ StaticFileProviderFactory
+ HeaderSyncGapProvider<Header = SnapSyncHeader<F>>
+ Clone
+ Send
+ Sync
+ 'static,
F::Provider: DBProvider + HeaderProvider + StorageSettingsCache,
F::ProviderRW: DBProvider
+ NodePrimitivesProvider
+ StateWriter
+ StaticFileProviderFactory
+ StageCheckpointWriter,
<F::Provider as DBProvider>::Tx: DbTx,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
SnapSyncHeader<F>: Value + FullBlockHeader,
{
/// Starts snap sync.
///
/// Returns `false` if snap sync is already active.
pub fn start(&mut self, target_hash: alloy_primitives::B256) -> bool {
if self.is_active() {
return false;
}
let (result_tx, result_rx) = oneshot::channel();
let client = self.client.clone();
let factory = self.factory.clone();
self.runtime.spawn_critical_blocking_task("snap sync header download", async move {
let result = Self::run_header_stage(client, factory, target_hash).await;
let _ = result_tx.send(result);
});
self.target_hash = Some(target_hash);
self.state = Some(SnapSyncState::DownloadingHeaders { result_rx });
true
}
/// Phase A: resolve the pivot from peers and run `HeaderStage` to fill
/// static files with headers 1..pivot.
async fn run_header_stage(
client: C,
factory: F,
target_hash: alloy_primitives::B256,
) -> Result<(), String> {
tracing::info!(target: "sync::snap", %target_hash, "Phase A: resolving target header from peers");
let target_header = client
.get_header(alloy_eips::BlockHashOrNumber::Hash(target_hash))
.await
.map_err(|e| format!("failed to fetch target header: {e}"))?
.into_data()
.ok_or_else(|| "peer returned empty response for target header".to_string())?;
let target_number = target_header.number();
let pivot_number = target_number.saturating_sub(crate::PIVOT_OFFSET);
if pivot_number == 0 {
tracing::info!(target: "sync::snap", "Target too low for header stage, skipping Phase A");
return Ok(());
}
let pivot_header = client
.get_header(alloy_eips::BlockHashOrNumber::Number(pivot_number))
.await
.map_err(|e| format!("failed to fetch pivot header: {e}"))?
.into_data()
.ok_or_else(|| "peer returned empty response for pivot header".to_string())?;
let pivot_sealed = reth_primitives_traits::SealedHeader::seal_slow(pivot_header);
let pivot_hash = pivot_sealed.hash();
tracing::info!(
target: "sync::snap",
target_number,
pivot_number,
%pivot_hash,
"Phase A: downloading headers 1..{pivot_number}"
);
let (_tip_tx, tip_rx) = tokio::sync::watch::channel(pivot_hash);
let downloader =
ReverseHeadersDownloaderBuilder::default().build(client, NoopConsensus::arc());
let mut stage = HeaderStage::new(factory.clone(), downloader, tip_rx, EtlConfig::default());
let input =
ExecInput { target: Some(pivot_number), checkpoint: Some(StageCheckpoint::new(0)) };
<HeaderStage<F, _> as StageExt<F::ProviderRW>>::execute_ready(&mut stage, input)
.await
.map_err(|e| format!("header download failed: {e}"))?;
let provider_rw = factory.database_provider_rw().map_err(|e| format!("db error: {e}"))?;
let _output = Stage::<F::ProviderRW>::execute(&mut stage, &provider_rw, input)
.map_err(|e| format!("header write failed: {e}"))?;
provider_rw
.save_stage_checkpoint(StageId::Headers, StageCheckpoint::new(pivot_number))
.map_err(|e| format!("checkpoint save failed: {e}"))?;
provider_rw.commit().map_err(|e| format!("commit failed: {e}"))?;
factory
.static_file_provider()
.commit()
.map_err(|e| format!("static file commit failed: {e}"))?;
tracing::info!(target: "sync::snap", pivot_number, "Phase A complete: headers written to static files");
Ok(())
}
/// Spawn the Phase B orchestrator and return the started event.
fn start_orchestrator(&mut self) -> SnapSyncControlEvent {
let target_hash = self.target_hash.take().expect("target_hash set during start()");
let (events_tx, events_rx) = tokio::sync::mpsc::unbounded_channel();
let (result_tx, result_rx) = oneshot::channel();
let orchestrator = crate::orchestrator::SnapSyncOrchestrator::new(
self.client.clone(),
self.factory.clone(),
);
self.runtime.spawn_critical_blocking_task("snap sync orchestrator", async move {
let result = orchestrator.run(events_rx, target_hash).await;
let _ = result_tx.send(result);
});
let started_tx = events_tx.clone();
self.state = Some(SnapSyncState::DownloadingState { events_tx, result_rx });
SnapSyncControlEvent::Started(started_tx)
}
/// Polls the controller for the next lifecycle event.
pub fn poll(&mut self, cx: &mut Context<'_>) -> Poll<SnapSyncControlEvent> {
let Some(state) = &mut self.state else {
return Poll::Pending;
};
match state {
SnapSyncState::DownloadingHeaders { result_rx } => match result_rx.poll_unpin(cx) {
Poll::Ready(Ok(Ok(()))) => Poll::Ready(self.start_orchestrator()),
Poll::Ready(Ok(Err(e))) => {
self.state = None;
self.target_hash = None;
Poll::Ready(SnapSyncControlEvent::TaskDropped(format!(
"snap sync header download failed: {e}"
)))
}
Poll::Ready(Err(_)) => {
self.state = None;
self.target_hash = None;
Poll::Ready(SnapSyncControlEvent::TaskDropped(
"snap sync header download task dropped".into(),
))
}
Poll::Pending => Poll::Pending,
},
SnapSyncState::DownloadingState { result_rx, .. } => match result_rx.poll_unpin(cx) {
Poll::Ready(Ok(result)) => {
self.state = None;
self.target_hash = None;
Poll::Ready(SnapSyncControlEvent::Finished(result))
}
Poll::Ready(Err(_)) => {
self.state = None;
self.target_hash = None;
Poll::Ready(SnapSyncControlEvent::TaskDropped("snap sync task dropped".into()))
}
Poll::Pending => Poll::Pending,
},
}
}
}
impl<C, F> SnapSyncControl for SnapSyncController<C, F>
where
C: SnapClient + HeadersClient<Header = SnapSyncHeader<F>> + Clone + Send + Sync + 'static,
F: DatabaseProviderFactory
+ StaticFileProviderFactory
+ HeaderSyncGapProvider<Header = SnapSyncHeader<F>>
+ Clone
+ Send
+ Sync
+ 'static,
F::Provider: DBProvider + HeaderProvider + StorageSettingsCache,
F::ProviderRW: DBProvider
+ NodePrimitivesProvider
+ StateWriter
+ StaticFileProviderFactory
+ StageCheckpointWriter,
<F::Provider as DBProvider>::Tx: DbTx,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
SnapSyncHeader<F>: Value + FullBlockHeader,
{
fn is_active(&self) -> bool {
SnapSyncController::is_active(self)
}
fn start(&mut self, target_hash: alloy_primitives::B256) -> bool {
SnapSyncController::start(self, target_hash)
}
fn poll(&mut self, cx: &mut Context<'_>) -> Poll<SnapSyncControlEvent> {
SnapSyncController::poll(self, cx)
}
}

View File

@@ -0,0 +1,650 @@
//! Snap sync download loops for accounts, storage, and bytecodes.
//!
//! The main entry point is [`download_state`] which streams through the entire
//! state trie in account-hash order. For each batch of accounts it immediately
//! fetches the associated storage slots and bytecodes before moving on to the
//! next range. This keeps memory usage bounded to a single batch at a time
//! regardless of total state size.
use crate::{
proof::verify_range_proof,
storage::{increment_b256, write_bytecodes, write_hashed_accounts, write_hashed_storages},
SnapSyncError, SNAP_RESPONSE_BYTES_LIMIT,
};
use alloy_primitives::{keccak256, Bytes, B256, U256};
use reth_db_api::transaction::DbTxMut;
use reth_eth_wire_types::snap::{
GetAccountRangeMessage, GetByteCodesMessage, GetStorageRangesMessage, StorageData, TrieAccount,
};
use reth_network_p2p::snap::client::{SnapClient, SnapResponse};
use reth_primitives_traits::Account;
use reth_provider::{DatabaseProviderFactory, HeaderProvider};
use reth_storage_api::{DBProvider, StateWriter};
use reth_trie::root::storage_root;
use std::collections::{HashMap, HashSet};
use tracing::info;
/// Maximum number of account hashes per storage range request.
const STORAGE_BATCH_SIZE: usize = 20;
/// Maximum number of code hashes per bytecode request.
const BYTECODE_BATCH_SIZE: usize = 50;
/// Maximum hash value used as the range upper bound.
const MAX_HASH: B256 = B256::new([0xff; 32]);
type DecodedStorageSlots = Vec<(B256, U256)>;
// ──────────────────────────────────────────────────────────────────────────────
// Streaming state download
// ──────────────────────────────────────────────────────────────────────────────
/// Result of a [`download_state`] call.
#[derive(Debug)]
pub enum DownloadStateOutcome {
/// Entire account range was iterated — download is complete.
Done,
/// The serving peer returned empty for the requested root (stale).
/// Contains the `starting_hash` to resume from after the caller
/// advances the pivot and obtains a fresh root.
Stale {
/// The hash to resume downloading from with a fresh root.
resume_from: B256,
},
}
/// Downloads state (accounts, storage, bytecodes) at `root_hash`, streaming
/// from `starting_hash` onward.
///
/// For each batch of accounts returned by `GetAccountRange`, the associated
/// storage and bytecodes are fetched and written to MDBX immediately before
/// requesting the next account range. Memory usage is bounded to one batch.
///
/// Returns [`DownloadStateOutcome::Stale`] when the serving peer returns empty
/// (root not available). The caller should advance the pivot to get a new root
/// and call this again with the returned `resume_from` hash — no progress is
/// lost.
pub async fn download_state<C, F>(
client: &C,
factory: &F,
root_hash: B256,
starting_hash: B256,
) -> Result<DownloadStateOutcome, SnapSyncError>
where
C: SnapClient + 'static,
F: DatabaseProviderFactory + Clone + Send + Sync + 'static,
F::Provider: DBProvider + HeaderProvider,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let mut request_id: u64 = 0;
let mut cursor = starting_hash;
loop {
// Remember the start of this batch so we can resume here if any
// sub-fetch (accounts, storage, bytecodes) hits a stale root.
let batch_start = cursor;
// ── Fetch account batch ──────────────────────────────────────────
request_id += 1;
let request = GetAccountRangeMessage {
request_id,
root_hash,
starting_hash: cursor,
limit_hash: MAX_HASH,
response_bytes: SNAP_RESPONSE_BYTES_LIMIT,
};
let response = client.get_account_range(request).await.map_err(|e| {
SnapSyncError::Network(format!("snap account range request failed: {e}"))
})?;
let msg = match response.into_data() {
SnapResponse::AccountRange(msg) => msg,
_ => return Err(SnapSyncError::Network("unexpected snap response type".into())),
};
if msg.accounts.is_empty() {
if msg.proof.is_empty() {
return Ok(DownloadStateOutcome::Stale { resume_from: cursor });
}
verify_account_range_proof(root_hash, cursor, &[], &msg.proof)?;
return Ok(DownloadStateOutcome::Done);
}
// ── Decode + write accounts ──────────────────────────────────────
let mut account_batch = Vec::with_capacity(msg.accounts.len());
let mut batch_account_hashes = Vec::with_capacity(msg.accounts.len());
let mut batch_code_hashes = HashSet::new();
let mut batch_storage_roots = HashMap::with_capacity(msg.accounts.len());
let mut decoded_accounts = Vec::with_capacity(msg.accounts.len());
let mut previous_hash = None;
for account_data in &msg.accounts {
if account_data.hash < cursor ||
previous_hash.is_some_and(|previous| account_data.hash <= previous)
{
return Err(SnapSyncError::Network(
"snap account range returned non-monotonic account hashes".into(),
));
}
let trie_account = account_data.account;
let account = Account::from(trie_account);
if let Some(code_hash) = account.bytecode_hash {
batch_code_hashes.insert(code_hash);
}
previous_hash = Some(account_data.hash);
batch_storage_roots.insert(account_data.hash, trie_account.storage_root);
decoded_accounts.push((account_data.hash, trie_account));
batch_account_hashes.push(account_data.hash);
account_batch.push((account_data.hash, account));
}
verify_account_range_proof(root_hash, cursor, &decoded_accounts, &msg.proof)?;
info!(
target: "engine::snap_sync",
accounts = account_batch.len(),
%root_hash,
"Downloaded account range"
);
write_hashed_accounts(factory, &account_batch)?;
// ── Fetch + write storage for this batch ─────────────────────────
// If the peer returns empty (stale root), return Stale at batch_start
// so the caller retries the entire batch with a fresh root.
if fetch_storage_for_accounts(
client,
factory,
root_hash,
&batch_account_hashes,
&batch_storage_roots,
&mut request_id,
)
.await?
{
return Ok(DownloadStateOutcome::Stale { resume_from: batch_start });
}
// ── Fetch + write bytecodes for this batch ───────────────────────
fetch_bytecodes(client, factory, &batch_code_hashes, &mut request_id).await?;
// ── Advance cursor ───────────────────────────────────────────────
let last_hash = msg.accounts.last().expect("checked non-empty above").hash;
if last_hash == MAX_HASH {
return Ok(DownloadStateOutcome::Done);
}
cursor = increment_b256(last_hash);
}
}
// ──────────────────────────────────────────────────────────────────────────────
// Storage download (per-batch)
// ──────────────────────────────────────────────────────────────────────────────
/// Fetches and writes storage for a batch of account hashes.
///
/// Returns `Ok(true)` if the serving peer returned empty (stale root),
/// `Ok(false)` if all storage was fetched successfully.
///
/// Handles the snap protocol's response-size truncation: if the last account
/// in a multi-account response has a proof attached, its storage was incomplete
/// and we issue continuation requests for that account before moving on.
async fn fetch_storage_for_accounts<C, F>(
client: &C,
factory: &F,
root_hash: B256,
account_hashes: &[B256],
storage_roots: &HashMap<B256, B256>,
request_id: &mut u64,
) -> Result<bool, SnapSyncError>
where
C: SnapClient + 'static,
F: DatabaseProviderFactory + Clone + Send + Sync + 'static,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let mut idx = 0;
while idx < account_hashes.len() {
let end = (idx + STORAGE_BATCH_SIZE).min(account_hashes.len());
let chunk = &account_hashes[idx..end];
*request_id += 1;
let request = GetStorageRangesMessage {
request_id: *request_id,
root_hash,
account_hashes: chunk.to_vec(),
starting_hash: B256::ZERO,
limit_hash: MAX_HASH,
response_bytes: SNAP_RESPONSE_BYTES_LIMIT,
};
let response = client.get_storage_ranges(request).await.map_err(|e| {
SnapSyncError::Network(format!("snap storage range request failed: {e}"))
})?;
let msg = match response.into_data() {
SnapResponse::StorageRanges(msg) => msg,
_ => return Err(SnapSyncError::Network("unexpected snap response type".into())),
};
if msg.slots.len() > chunk.len() {
return Err(SnapSyncError::Network(
"snap storage range returned more slot lists than requested".into(),
));
}
let returned_count = msg.slots.len();
// Empty response for the very first sub-chunk → stale root.
if returned_count == 0 && idx == 0 {
return Ok(true);
}
if returned_count == 0 {
return Err(SnapSyncError::Network("snap storage range returned no progress".into()));
}
let has_proof = !msg.proof.is_empty();
let proof_index = has_proof.then_some(returned_count - 1);
let mut entries = Vec::new();
for (i, slots) in msg.slots.iter().enumerate() {
let account_hash = chunk[i];
validate_storage_slots(account_hash, B256::ZERO, MAX_HASH, slots)?;
let account_slots = if Some(i) == proof_index {
verify_storage_range_proof(
account_hash,
storage_roots,
B256::ZERO,
slots,
&msg.proof,
)?;
if slots.is_empty() {
verify_full_storage_range(account_hash, storage_roots, slots)?
} else {
let resume_from =
increment_b256(slots.last().expect("slots is not empty").hash);
match fetch_storage_continuation(
client,
root_hash,
account_hash,
storage_roots,
resume_from,
request_id,
slots.clone(),
)
.await?
{
StorageContinuationOutcome::Complete(slots) => slots,
StorageContinuationOutcome::Stale => return Ok(true),
}
}
} else {
verify_full_storage_range(account_hash, storage_roots, slots)?
};
entries.extend(
account_slots
.into_iter()
.map(|(slot_hash, value)| (account_hash, slot_hash, value)),
);
}
if !entries.is_empty() {
write_hashed_storages(factory, &entries)?;
}
if has_proof {
idx += returned_count;
} else if returned_count < chunk.len() {
idx += returned_count;
} else {
idx = end;
}
}
Ok(false)
}
/// Continuation result for a single-account storage range.
enum StorageContinuationOutcome {
/// The account storage is complete and verified against its root.
Complete(DecodedStorageSlots),
/// The serving peer no longer has the requested root or account.
Stale,
}
/// Continuation loop for a single account whose storage was truncated.
async fn fetch_storage_continuation<C>(
client: &C,
root_hash: B256,
account_hash: B256,
storage_roots: &HashMap<B256, B256>,
mut starting_hash: B256,
request_id: &mut u64,
mut collected_slots: Vec<StorageData>,
) -> Result<StorageContinuationOutcome, SnapSyncError>
where
C: SnapClient + 'static,
{
loop {
*request_id += 1;
let request = GetStorageRangesMessage {
request_id: *request_id,
root_hash,
account_hashes: vec![account_hash],
starting_hash,
limit_hash: MAX_HASH,
response_bytes: SNAP_RESPONSE_BYTES_LIMIT,
};
let response = client.get_storage_ranges(request).await.map_err(|e| {
SnapSyncError::Network(format!("snap storage continuation failed: {e}"))
})?;
let msg = match response.into_data() {
SnapResponse::StorageRanges(msg) => msg,
_ => return Err(SnapSyncError::Network("unexpected snap response type".into())),
};
if msg.slots.len() > 1 {
return Err(SnapSyncError::Network(
"snap storage continuation returned multiple slot lists".into(),
));
}
let Some(slots) = msg.slots.first() else {
return Ok(StorageContinuationOutcome::Stale);
};
if slots.is_empty() {
if !msg.proof.is_empty() {
verify_storage_range_proof(
account_hash,
storage_roots,
starting_hash,
slots,
&msg.proof,
)?;
}
let decoded = verify_full_storage_range(account_hash, storage_roots, &collected_slots)?;
return Ok(StorageContinuationOutcome::Complete(decoded));
}
validate_storage_slots(account_hash, starting_hash, MAX_HASH, slots)?;
if !msg.proof.is_empty() {
verify_storage_range_proof(
account_hash,
storage_roots,
starting_hash,
slots,
&msg.proof,
)?;
}
collected_slots.extend_from_slice(slots);
if msg.proof.is_empty() {
let decoded = verify_full_storage_range(account_hash, storage_roots, &collected_slots)?;
return Ok(StorageContinuationOutcome::Complete(decoded));
}
starting_hash = increment_b256(slots.last().unwrap().hash);
}
}
fn validate_storage_slots(
account_hash: B256,
starting_hash: B256,
limit_hash: B256,
slots: &[StorageData],
) -> Result<(), SnapSyncError> {
let mut previous = None;
for slot in slots {
if slot.hash < starting_hash || slot.hash >= limit_hash {
return Err(SnapSyncError::Network(format!(
"snap storage range for account {account_hash} returned slot outside requested bounds"
)))
}
if previous.is_some_and(|previous| slot.hash <= previous) {
return Err(SnapSyncError::Network(format!(
"snap storage range for account {account_hash} returned non-monotonic slots"
)))
}
previous = Some(slot.hash);
}
Ok(())
}
fn verify_full_storage_range(
account_hash: B256,
storage_roots: &HashMap<B256, B256>,
slots: &[StorageData],
) -> Result<DecodedStorageSlots, SnapSyncError> {
let Some(expected_root) = storage_roots.get(&account_hash).copied() else {
return Err(SnapSyncError::Network(format!(
"snap storage response for unknown account {account_hash}"
)));
};
let decoded = decode_storage_slots(slots)?;
let got = storage_root(decoded.iter().copied());
if got != expected_root {
return Err(SnapSyncError::Network(format!(
"snap full storage range root mismatch for account {account_hash}: expected {expected_root}, got {got}"
)))
}
Ok(decoded)
}
fn verify_account_range_proof(
root_hash: B256,
starting_hash: B256,
accounts: &[(B256, TrieAccount)],
proof: &[Bytes],
) -> Result<(), SnapSyncError> {
let leaves =
accounts.iter().copied().map(|(hash, account)| (hash, account_trie_value(account)));
verify_range_proof(root_hash, starting_hash, leaves, proof)
.map_err(|e| SnapSyncError::Network(format!("invalid snap account range proof: {e}")))
}
fn verify_storage_range_proof(
account_hash: B256,
storage_roots: &HashMap<B256, B256>,
starting_hash: B256,
slots: &[StorageData],
proof: &[Bytes],
) -> Result<DecodedStorageSlots, SnapSyncError> {
let Some(storage_root) = storage_roots.get(&account_hash).copied() else {
return Err(SnapSyncError::Network(format!(
"snap storage proof for unknown account {account_hash}"
)));
};
let decoded = decode_storage_slots(slots)?;
let leaves = decoded
.iter()
.map(|(hash, value)| (*hash, alloy_rlp::encode_fixed_size(value).as_ref().to_vec()));
verify_range_proof(storage_root, starting_hash, leaves, proof)
.map_err(|e| SnapSyncError::Network(format!("invalid snap storage range proof: {e}")))?;
Ok(decoded)
}
fn account_trie_value(account: TrieAccount) -> Vec<u8> {
alloy_rlp::encode(account)
}
fn decode_storage_slots(slots: &[StorageData]) -> Result<DecodedStorageSlots, SnapSyncError> {
slots
.iter()
.map(|slot| {
let value = slot
.decode_value()
.map_err(|e| SnapSyncError::RlpDecode(format!("snap storage decode: {e}")))?;
Ok((slot.hash, value))
})
.collect()
}
// ──────────────────────────────────────────────────────────────────────────────
// Bytecode download (per-batch)
// ──────────────────────────────────────────────────────────────────────────────
/// Fetches and writes bytecodes for a set of code hashes.
async fn fetch_bytecodes<C, F>(
client: &C,
factory: &F,
code_hashes: &HashSet<B256>,
request_id: &mut u64,
) -> Result<(), SnapSyncError>
where
C: SnapClient + 'static,
F: DatabaseProviderFactory + Clone + Send + Sync + 'static,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let hashes: Vec<B256> = code_hashes.iter().copied().collect();
for chunk in hashes.chunks(BYTECODE_BATCH_SIZE) {
*request_id += 1;
let request = GetByteCodesMessage {
request_id: *request_id,
hashes: chunk.to_vec(),
response_bytes: SNAP_RESPONSE_BYTES_LIMIT,
};
let response = client
.get_byte_codes(request)
.await
.map_err(|e| SnapSyncError::Network(format!("snap bytecode request failed: {e}")))?;
let msg = match response.into_data() {
SnapResponse::ByteCodes(msg) => msg,
_ => return Err(SnapSyncError::Network("unexpected snap response type".into())),
};
let codes = match_bytecodes_to_hashes(chunk, &msg.codes)?;
if !codes.is_empty() {
write_bytecodes(factory, &codes)?;
}
}
Ok(())
}
fn match_bytecodes_to_hashes(
requested_hashes: &[B256],
codes: &[Bytes],
) -> Result<Vec<(B256, Bytes)>, SnapSyncError> {
let requested: HashMap<_, _> =
requested_hashes.iter().copied().enumerate().map(|(i, hash)| (hash, i)).collect();
let mut seen = HashSet::new();
let mut last_position = None;
let mut matched = Vec::with_capacity(codes.len());
for code in codes {
let hash = keccak256(code.as_ref());
let Some(position) = requested.get(&hash).copied() else {
return Err(SnapSyncError::Network(format!(
"snap bytecode response contained unrequested code hash {hash}"
)))
};
if last_position.is_some_and(|last| position <= last) {
return Err(SnapSyncError::Network(
"snap bytecode response was not in request order".into(),
));
}
if !seen.insert(hash) {
return Err(SnapSyncError::Network(format!(
"snap bytecode response duplicated code hash {hash}"
)))
}
last_position = Some(position);
matched.push((hash, code.clone()));
}
Ok(matched)
}
#[cfg(test)]
mod tests {
use super::*;
fn b256_from_u64(value: u64) -> B256 {
B256::left_padding_from(&value.to_be_bytes())
}
#[test]
fn bytecode_matching_uses_returned_code_hashes() {
let first = Bytes::from_static(&[1, 2, 3]);
let second = Bytes::from_static(&[4, 5, 6]);
let requested = vec![keccak256(second.as_ref()), keccak256(first.as_ref())];
let matched = match_bytecodes_to_hashes(&requested, &[first.clone()]).unwrap();
assert_eq!(matched, vec![(keccak256(first.as_ref()), first)]);
}
#[test]
fn bytecode_matching_rejects_unrequested_code() {
let requested = vec![keccak256([1, 2, 3])];
let unrequested = Bytes::from_static(&[4, 5, 6]);
assert!(match_bytecodes_to_hashes(&requested, &[unrequested]).is_err());
}
#[test]
fn bytecode_matching_rejects_out_of_order_codes() {
let first = Bytes::from_static(&[1, 2, 3]);
let second = Bytes::from_static(&[4, 5, 6]);
let requested = vec![keccak256(first.as_ref()), keccak256(second.as_ref())];
assert!(match_bytecodes_to_hashes(&requested, &[second, first]).is_err());
}
#[test]
fn storage_slots_must_be_ordered_within_bounds() {
let account = b256_from_u64(1);
let first = StorageData::from_value(b256_from_u64(2), alloy_primitives::U256::from(2));
let second = StorageData::from_value(b256_from_u64(3), alloy_primitives::U256::from(3));
assert!(validate_storage_slots(
account,
b256_from_u64(2),
b256_from_u64(4),
&[first.clone(), second.clone()]
)
.is_ok());
assert!(validate_storage_slots(
account,
b256_from_u64(2),
b256_from_u64(4),
&[second, first]
)
.is_err());
}
#[test]
fn full_storage_range_verifies_storage_root() {
let account = b256_from_u64(1);
let slot = b256_from_u64(2);
let value = alloy_primitives::U256::from(3);
let storage_roots = HashMap::from([(account, storage_root([(slot, value)]))]);
let slots = vec![StorageData::from_value(slot, value)];
assert!(verify_full_storage_range(account, &storage_roots, &slots).is_ok());
assert!(verify_full_storage_range(account, &storage_roots, &[]).is_err());
}
}

View File

@@ -0,0 +1,68 @@
//! Sync finalization: stage checkpoints and static file segment advancement.
use crate::{storage::db_err, SnapSyncError};
use reth_db_api::{tables, transaction::DbTxMut};
use reth_provider::{
DatabaseProviderFactory, StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
};
use reth_stages_types::{StageCheckpoint, StageId};
use reth_storage_api::DBProvider;
/// Writes stage checkpoints for all stages that snap sync satisfies.
///
/// After BAL healing completes, the database state corresponds to `target_block`.
/// This records that fact so the pipeline can resume from the correct point.
pub(crate) fn write_snap_stage_checkpoints<F>(
factory: &F,
target_block: u64,
) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider + StaticFileProviderFactory,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let checkpoint = StageCheckpoint::new(target_block);
let stages = [
StageId::Bodies,
StageId::SenderRecovery,
StageId::Execution,
StageId::AccountHashing,
StageId::StorageHashing,
StageId::TransactionLookup,
StageId::IndexAccountHistory,
StageId::IndexStorageHistory,
];
let provider = factory.database_provider_rw().map_err(db_err)?;
{
let tx = provider.tx_ref();
for stage_id in stages {
tx.put::<tables::StageCheckpoints>(stage_id.to_string(), checkpoint).map_err(db_err)?;
}
}
// Advance static file segments that snap sync did not populate (headers are
// already filled by Phase A). Without this, the persistence service would fail
// with `UnexpectedStaticFileBlockNumber` when writing blocks after the snap
// target because these segments would still be at block 0.
let segments = [
StaticFileSegment::Transactions,
StaticFileSegment::TransactionSenders,
StaticFileSegment::Receipts,
StaticFileSegment::AccountChangeSets,
StaticFileSegment::StorageChangeSets,
];
let sfp = provider.static_file_provider();
for segment in segments {
let mut writer = sfp.get_writer(0, segment).map_err(|e| {
SnapSyncError::Database(format!("static file writer for {segment:?}: {e}"))
})?;
writer.ensure_at_block(target_block).map_err(|e| {
SnapSyncError::Database(format!("ensure_at_block({target_block}) for {segment:?}: {e}"))
})?;
}
sfp.commit().map_err(|e| SnapSyncError::Database(format!("static file commit: {e}")))?;
provider.commit().map_err(db_err)?;
Ok(())
}

View File

@@ -0,0 +1,118 @@
//! Engine-driven snap sync orchestrator for snap/2 (EIP-8189).
//!
//! This crate implements a standalone snap sync process driven by the engine tree,
//! not the staged pipeline. Snap sync is a live, reactive process that responds to
//! chain advancement in real-time via events forwarded from the engine.
pub mod bal;
pub mod controller;
pub mod download;
pub mod finalize;
pub mod orchestrator;
pub mod pivot;
pub mod serve;
pub mod storage;
mod proof;
use alloy_primitives::{Bytes, B256};
/// How many blocks behind HEAD to place the snap sync pivot.
///
/// The serving node reverse-applies changesets to reconstruct hashed state at
/// HEADN, so this must be large enough that the target block's hashed state
/// is always fully persisted to MDBX (the engine keeps ~2 blocks in memory).
pub const PIVOT_OFFSET: u64 = 16;
/// Soft response size limit for snap protocol requests (2 MiB).
pub const SNAP_RESPONSE_BYTES_LIMIT: u64 = 2 * 1024 * 1024;
/// Events sent from the engine tree to the snap sync orchestrator.
#[derive(Debug, Clone)]
pub enum SnapSyncEvent {
/// A new block was received via `new_payload`. The BAL bytes come from `ExecutionPayloadV4`.
NewBlock {
/// Block number.
number: u64,
/// Block hash.
hash: B256,
/// State root from the block header.
state_root: B256,
/// Parent block hash.
parent_hash: B256,
/// RLP-encoded BAL bytes, if present in the payload.
bal: Option<Bytes>,
},
/// A block downloaded by the engine's block downloader.
/// Contains header info needed for persistence and BAL resolution.
DownloadedBlock {
/// Block number.
number: u64,
/// Block hash.
hash: B256,
/// State root from the block header.
state_root: B256,
/// Parent block hash.
parent_hash: B256,
},
/// The canonical head changed via `forkchoiceUpdated`.
NewHead {
/// Head block hash.
head_hash: B256,
},
}
/// Outcome reported by the orchestrator when snap sync completes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapSyncOutcome {
/// The block number that was synced to.
pub synced_to: u64,
/// Block hash of the synced-to block.
pub block_hash: B256,
}
/// Errors that can occur during snap sync.
#[derive(Debug, thiserror::Error)]
pub enum SnapSyncError {
/// A network request failed.
#[error("network request failed: {0}")]
Network(String),
/// Database operation failed.
#[error("database error: {0}")]
Database(String),
/// RLP decoding failed.
#[error("RLP decode error: {0}")]
RlpDecode(String),
/// BAL verification failed (hash mismatch).
#[error("BAL verification failed for block {block}: expected {expected}, got {got}")]
BalVerification {
/// Block number.
block: u64,
/// Expected hash from the header.
expected: B256,
/// Computed hash from the BAL bytes.
got: B256,
},
/// Header not found.
#[error("header not found for block {0}")]
MissingHeader(u64),
/// Block hash not found.
#[error("block hash not found for block {0}")]
MissingBlockHash(u64),
/// State root mismatch after sync.
#[error("state root mismatch at block {block}: expected {expected}, computed {computed}")]
StateRootMismatch {
/// Block number.
block: u64,
/// Expected state root from header.
expected: B256,
/// Computed state root.
computed: B256,
},
/// Event channel closed unexpectedly.
#[error("event channel closed")]
ChannelClosed,
/// BAL not available for a required block.
#[error("BAL not available for block {0}")]
MissingBal(u64),
}

View File

@@ -0,0 +1,277 @@
//! Snap sync orchestrator — the main async loop that drives snap sync from start to finish.
use crate::{
bal::{bal_to_state_diff, merge_account_diff},
download::{download_state, DownloadStateOutcome},
finalize::write_snap_stage_checkpoints,
pivot::PivotTracker,
storage::{
clear_hashed_state, read_hashed_account, write_bytecodes, write_hashed_accounts,
write_hashed_storages,
},
SnapSyncError, SnapSyncEvent, SnapSyncOutcome, PIVOT_OFFSET,
};
use alloy_consensus::BlockHeader;
use alloy_eips::BlockHashOrNumber;
use alloy_primitives::B256;
use reth_db_api::transaction::{DbTx, DbTxMut};
use reth_network_p2p::{headers::client::HeadersClient, snap::client::SnapClient};
use reth_primitives_traits::SealedHeader;
use reth_provider::{DatabaseProviderFactory, HeaderProvider};
use reth_storage_api::{DBProvider, StateWriter, StorageSettingsCache};
use tokio::sync::mpsc::UnboundedReceiver;
/// Engine-driven snap sync orchestrator.
///
/// Runs as an async task spawned by the engine tree. Receives chain events
/// via an mpsc channel and drives the snap sync process through four phases:
///
/// 1. **Bootstrap** — wait for head, pick pivot, clear state
/// 2. **Bulk download** — download accounts, storage, bytecodes from peers
/// 3. **BAL catch-up** — apply remaining BAL diffs to reach latest known block
/// 4. **Verification** — compute and verify state root
#[derive(Debug)]
pub struct SnapSyncOrchestrator<C, F> {
client: C,
factory: F,
}
impl<C, F> SnapSyncOrchestrator<C, F>
where
C: SnapClient + HeadersClient + Clone + Send + Sync + 'static,
F: DatabaseProviderFactory + Clone + Send + Sync + 'static,
F::Provider: DBProvider + HeaderProvider + StorageSettingsCache,
F::ProviderRW: DBProvider + StateWriter + reth_provider::StaticFileProviderFactory,
<F::Provider as DBProvider>::Tx: DbTx,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
/// Creates a new orchestrator with the given network client and database factory.
pub fn new(client: C, factory: F) -> Self {
Self { client, factory }
}
/// Runs the snap sync orchestrator to completion.
///
/// This is the main entry point, intended to be spawned as a tokio task.
/// `target_hash` is the FCU head block hash — used to resolve the head from
/// peers when no `NewHead` event arrives (e.g. frozen-head / fresh-node scenario).
pub async fn run(
self,
mut events_rx: UnboundedReceiver<SnapSyncEvent>,
target_hash: B256,
) -> Result<SnapSyncOutcome, SnapSyncError> {
// ── Phase 0: Bootstrap — wait for head, pick pivot, clear state ──────
tracing::info!(target: "engine::snap_sync", %target_hash, "Starting snap sync orchestrator");
let mut pre_buffered_blocks = Vec::new();
let (initial_head_number, initial_head_hash) = loop {
// Try to receive a NewHead from the engine tree first (non-blocking drain)
match events_rx.try_recv() {
Ok(SnapSyncEvent::NewHead { head_hash }) => {
let header = self
.client
.get_header(BlockHashOrNumber::Hash(head_hash))
.await
.map_err(|e| SnapSyncError::Network(format!("header fetch failed: {e}")))?
.into_data()
.ok_or_else(|| {
SnapSyncError::Network(format!(
"peer returned empty response for header {head_hash}"
))
})?;
break (header.number(), head_hash);
}
Ok(event @ SnapSyncEvent::NewBlock { .. }) |
Ok(event @ SnapSyncEvent::DownloadedBlock { .. }) => {
pre_buffered_blocks.push(event);
continue;
}
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
return Err(SnapSyncError::ChannelClosed);
}
}
// No NewHead available yet — resolve from peers using the target hash.
tracing::info!(
target: "engine::snap_sync",
%target_hash,
"No NewHead event, resolving target header from peers"
);
let header = self
.client
.get_header(BlockHashOrNumber::Hash(target_hash))
.await
.map_err(|e| SnapSyncError::Network(format!("header fetch failed: {e}")))?
.into_data()
.ok_or_else(|| {
SnapSyncError::Network(format!(
"peer returned empty response for header {target_hash}"
))
})?;
break (header.number(), target_hash);
};
let pivot_block = initial_head_number.saturating_sub(PIVOT_OFFSET);
let initial_pivot = pivot_block;
let pivot_root = {
let from_buffer = pre_buffered_blocks.iter().find_map(|e| match e {
SnapSyncEvent::NewBlock { number, state_root, .. } |
SnapSyncEvent::DownloadedBlock { number, state_root, .. }
if *number == pivot_block =>
{
Some(*state_root)
}
_ => None,
});
match from_buffer {
Some(root) => root,
None => {
// Try local DB first, fall back to fetching from peers
let local = self
.factory
.database_provider_ro()
.ok()
.and_then(|p| p.header_by_number(pivot_block).ok().flatten());
match local {
Some(h) => h.state_root(),
None => {
let h = self
.client
.get_header(BlockHashOrNumber::Number(pivot_block))
.await
.map_err(|e| {
SnapSyncError::Network(format!(
"pivot header fetch failed: {e}"
))
})?
.into_data()
.ok_or(SnapSyncError::MissingHeader(pivot_block))?;
h.state_root()
}
}
}
}
};
tracing::info!(
target: "engine::snap_sync",
pivot_block,
%pivot_root,
head = initial_head_number,
"Picked pivot"
);
let mut tracker = PivotTracker::new(pivot_block, pivot_root, events_rx);
tracker.set_known_head(initial_head_number, initial_head_hash);
for event in pre_buffered_blocks {
tracker.buffer_event(event);
}
clear_hashed_state(&self.factory)?;
tracing::info!(target: "engine::snap_sync", "Cleared hashed state tables");
// ── Phase 1: Bulk state download ─────────────────────────────────────
//
// Stream accounts in hash order. If the serving peer returns empty
// (root is stale because chain advanced), advance the pivot to get a
// fresh root and resume from the same position.
tracing::info!(target: "engine::snap_sync", %pivot_root, "Phase 1: bulk state download");
let mut download_cursor = B256::ZERO;
loop {
let root = tracker.pivot_root();
match download_state(&self.client, &self.factory, root, download_cursor).await? {
DownloadStateOutcome::Done => break,
DownloadStateOutcome::Stale { resume_from } => {
tracing::info!(
target: "engine::snap_sync",
%root,
%resume_from,
"Pivot root stale, re-resolving head from peers"
);
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
// Drain any new events that arrived while we were sleeping.
tracker.drain_events();
// Try to discover chain advancement by probing a header
// a few blocks ahead of what we know. The serving node
// may have advanced beyond our initial head.
let probe = tracker.known_head() + 10;
if let Ok(response) =
self.client.get_header(BlockHashOrNumber::Number(probe)).await
{
if let Some(header) = response.into_data() {
let hash = SealedHeader::seal_slow(header).hash();
tracker.set_known_head(probe, hash);
}
}
tracker.advance_pivot(&self.client, &self.factory).await?;
download_cursor = resume_from;
}
}
}
tracing::info!(target: "engine::snap_sync", "Phase 1 complete: bulk download finished");
// ── Phase 2: BAL catch-up ────────────────────────────────────────────
tracing::info!(target: "engine::snap_sync", "Phase 2: BAL catch-up");
tracker.drain_events();
let final_block = tracker.known_head();
if final_block > initial_pivot {
for block_num in (initial_pivot + 1)..=final_block {
let bal = tracker.get_verified_bal(&self.client, &self.factory, block_num).await?;
let diff = bal_to_state_diff(&bal.account_changes);
let mut merged = Vec::with_capacity(diff.accounts.len());
for acct_diff in &diff.accounts {
let existing = read_hashed_account(&self.factory, acct_diff.hashed_address)?;
let account = merge_account_diff(acct_diff, existing.as_ref());
merged.push((acct_diff.hashed_address, account));
}
write_hashed_accounts(&self.factory, &merged)?;
write_hashed_storages(&self.factory, &diff.storage)?;
write_bytecodes(&self.factory, &diff.bytecodes)?;
tracing::info!(
target: "engine::snap_sync",
block = block_num,
bal_bytes_len = bal.bytes.len(),
account_changes_count = bal.account_changes.len(),
diff_accounts = diff.accounts.len(),
diff_storage = diff.storage.len(),
diff_bytecodes = diff.bytecodes.len(),
"Applied BAL catch-up diff"
);
}
}
tracing::info!(
target: "engine::snap_sync",
final_block,
"Phase 2 complete: BAL catch-up finished"
);
write_snap_stage_checkpoints(&self.factory, final_block)?;
tracing::info!(
target: "engine::snap_sync",
block = final_block,
"Snap sync complete — MerkleExecute stage will verify state root"
);
Ok(SnapSyncOutcome { synced_to: final_block, block_hash: tracker.known_head_hash() })
}
}

View File

@@ -0,0 +1,310 @@
//! Pivot tracking, advancement, and BAL buffering.
use crate::{SnapSyncError, SnapSyncEvent, SNAP_RESPONSE_BYTES_LIMIT};
use alloy_consensus::BlockHeader;
use alloy_eip7928::{compute_block_access_list_hash, AccountChanges};
use alloy_eips::BlockHashOrNumber;
use alloy_primitives::{Bytes, B256};
use alloy_rlp::Decodable;
use reth_db_api::transaction::DbTx;
use reth_eth_wire_types::snap::GetBlockAccessListsMessage;
use reth_network_p2p::{
headers::client::HeadersClient,
snap::client::{SnapClient, SnapResponse},
};
use reth_primitives_traits::SealedHeader;
use reth_provider::{DatabaseProviderFactory, HeaderProvider};
use reth_storage_api::DBProvider;
use std::collections::BTreeMap;
use tokio::sync::mpsc::UnboundedReceiver;
/// A block that has been received from the engine but not yet applied.
#[derive(Debug, Clone)]
struct BufferedBlock {
/// State root from the block header.
state_root: B256,
/// RLP-encoded BAL bytes, if present.
bal: Option<Bytes>,
}
/// A verified BAL and its decoded account changes.
pub(crate) struct VerifiedBal {
/// Original RLP-encoded BAL bytes.
pub bytes: Bytes,
/// Decoded account changes.
pub account_changes: Vec<AccountChanges>,
}
/// Tracks the current pivot block and buffers incoming BALs from the engine.
#[derive(Debug)]
pub struct PivotTracker {
/// Current pivot block number.
pivot_block: u64,
/// State root at the current pivot.
pivot_root: B256,
/// Known head block number (from NewHead events).
known_head: u64,
/// Known head hash.
known_head_hash: B256,
/// Buffered blocks received via `SnapSyncEvent::NewBlock`, keyed by block number.
buffered_blocks: BTreeMap<u64, BufferedBlock>,
/// Event receiver from the engine.
events_rx: UnboundedReceiver<SnapSyncEvent>,
}
impl PivotTracker {
/// Creates a new tracker with the given pivot and an empty buffer.
pub fn new(
pivot_block: u64,
pivot_root: B256,
events_rx: UnboundedReceiver<SnapSyncEvent>,
) -> Self {
Self {
pivot_block,
pivot_root,
known_head: 0,
known_head_hash: B256::ZERO,
buffered_blocks: BTreeMap::new(),
events_rx,
}
}
/// Returns the current pivot block number.
pub fn pivot_block(&self) -> u64 {
self.pivot_block
}
/// Returns the state root at the current pivot.
pub fn pivot_root(&self) -> B256 {
self.pivot_root
}
/// Returns the known head block number.
pub fn known_head(&self) -> u64 {
self.known_head
}
/// Returns the known head block hash.
pub fn known_head_hash(&self) -> B256 {
self.known_head_hash
}
/// Sets the known head block number and hash.
pub fn set_known_head(&mut self, number: u64, hash: B256) {
if number > self.known_head {
self.known_head = number;
self.known_head_hash = hash;
}
}
/// Processes a single event, buffering it into the tracker's state.
pub(crate) fn buffer_event(&mut self, event: SnapSyncEvent) {
match event {
SnapSyncEvent::NewBlock { number, state_root, bal, .. } => {
self.buffered_blocks.insert(number, BufferedBlock { state_root, bal });
}
SnapSyncEvent::DownloadedBlock { number, state_root, .. } => {
self.buffered_blocks.insert(number, BufferedBlock { state_root, bal: None });
}
SnapSyncEvent::NewHead { head_hash } => {
// Hash-only: we don't update known_head number here.
// The orchestrator resolves the number from peers at bootstrap.
self.known_head_hash = head_hash;
}
}
}
/// Drains all pending events from the engine channel (non-blocking).
pub fn drain_events(&mut self) {
while let Ok(event) = self.events_rx.try_recv() {
self.buffer_event(event);
}
}
/// Advances the pivot to the latest known head without applying BAL diffs.
///
/// This only bumps the pivot block/root so that subsequent download requests
/// use a fresh root the serving peer can satisfy. BAL healing is done once
/// after all downloading completes (Phase 2 in the orchestrator).
///
/// Returns `Ok(true)` if the pivot was advanced, `Ok(false)` if already at head.
pub async fn advance_pivot<C, F>(
&mut self,
client: &C,
factory: &F,
) -> Result<bool, SnapSyncError>
where
C: HeadersClient + 'static,
F: DatabaseProviderFactory,
F::Provider: DBProvider + HeaderProvider,
<F::Provider as DBProvider>::Tx: DbTx,
{
self.drain_events();
let new_pivot = self.known_head.saturating_sub(crate::PIVOT_OFFSET);
if new_pivot <= self.pivot_block {
return Ok(false);
}
let old_pivot = self.pivot_block;
let new_root = self.resolve_state_root(client, factory, new_pivot).await?;
self.pivot_block = new_pivot;
self.pivot_root = new_root;
self.buffered_blocks = self.buffered_blocks.split_off(&(new_pivot.saturating_sub(10)));
tracing::info!(target: "engine::snap_sync", old_pivot, new_pivot, %new_root, "Advanced pivot");
Ok(true)
}
/// Gets and verifies a block BAL, checking the buffer first then fetching from peers.
pub(crate) async fn get_verified_bal<C, F>(
&self,
client: &C,
factory: &F,
block_num: u64,
) -> Result<VerifiedBal, SnapSyncError>
where
C: SnapClient + HeadersClient + 'static,
F: DatabaseProviderFactory + Clone + Send + Sync + 'static,
F::Provider: DBProvider + HeaderProvider,
<F::Provider as DBProvider>::Tx: DbTx,
{
let (block_hash, expected_hash) =
self.resolve_header_hash(client, factory, block_num).await?;
let bal = if let Some(block) = self.buffered_blocks.get(&block_num) {
block.bal.clone()
} else {
None
};
let bal = match bal {
Some(bal) => bal,
None => {
let response = client
.get_snap_block_access_lists(GetBlockAccessListsMessage {
request_id: 0,
block_hashes: vec![block_hash],
response_bytes: SNAP_RESPONSE_BYTES_LIMIT,
})
.await
.map_err(|e| {
SnapSyncError::Network(format!(
"snap/2 BAL fetch for block {block_num}: {e}"
))
})?;
let SnapResponse::BlockAccessLists(message) = response.into_data() else {
return Err(SnapSyncError::Network(format!(
"peer returned non-BAL snap response for block {block_num}"
)));
};
let bal = message
.block_access_lists
.0
.into_iter()
.next()
.ok_or(SnapSyncError::MissingBal(block_num))?;
if bal.as_ref() == [alloy_rlp::EMPTY_STRING_CODE] {
return Err(SnapSyncError::MissingBal(block_num));
}
bal
}
};
let account_changes: Vec<AccountChanges> = Vec::<AccountChanges>::decode(&mut bal.as_ref())
.map_err(|e| {
SnapSyncError::RlpDecode(format!("BAL decode at block {block_num}: {e}"))
})?;
let got = compute_block_access_list_hash(&account_changes);
if got != expected_hash {
return Err(SnapSyncError::BalVerification {
block: block_num,
expected: expected_hash,
got,
});
}
Ok(VerifiedBal { bytes: bal, account_changes })
}
async fn resolve_header_hash<C, F>(
&self,
client: &C,
factory: &F,
block_num: u64,
) -> Result<(B256, B256), SnapSyncError>
where
C: HeadersClient + 'static,
F: DatabaseProviderFactory,
F::Provider: DBProvider + HeaderProvider,
<F::Provider as DBProvider>::Tx: DbTx,
{
let local = factory
.database_provider_ro()
.ok()
.and_then(|p| p.header_by_number(block_num).ok().flatten());
match local {
Some(header) => {
let expected = header.block_access_list_hash().unwrap_or_default();
Ok((SealedHeader::seal_slow(header).hash(), expected))
}
None => {
let header = client
.get_header(BlockHashOrNumber::Number(block_num))
.await
.map_err(|e| {
SnapSyncError::Network(format!("header fetch for block {block_num}: {e}"))
})?
.into_data()
.ok_or(SnapSyncError::MissingHeader(block_num))?;
let expected = header.block_access_list_hash().unwrap_or_default();
Ok((SealedHeader::seal_slow(header).hash(), expected))
}
}
}
/// Resolves the state root for a block number from the buffer, local DB, or peers.
async fn resolve_state_root<C, F>(
&self,
client: &C,
factory: &F,
block_num: u64,
) -> Result<B256, SnapSyncError>
where
C: HeadersClient + 'static,
F: DatabaseProviderFactory,
F::Provider: DBProvider + HeaderProvider,
<F::Provider as DBProvider>::Tx: DbTx,
{
if let Some(block) = self.buffered_blocks.get(&block_num) {
return Ok(block.state_root);
}
// Try local DB first, fall back to fetching header from peers
let local = factory
.database_provider_ro()
.ok()
.and_then(|p| p.header_by_number(block_num).ok().flatten());
match local {
Some(header) => Ok(header.state_root()),
None => {
let header = client
.get_header(BlockHashOrNumber::Number(block_num))
.await
.map_err(|e| {
SnapSyncError::Network(format!(
"pivot header fetch for block {block_num}: {e}"
))
})?
.into_data()
.ok_or(SnapSyncError::MissingHeader(block_num))?;
Ok(header.state_root())
}
}
}
}

View File

@@ -0,0 +1,440 @@
//! Snap range proof verification.
//!
//! Snap range responses prove a consecutive leaf range with the boundary trie
//! nodes that connect the returned leaves to the rest of the trie. Verifying
//! only the first and last leaf is not enough: an adversarial peer could omit a
//! leaf in the middle while still proving both endpoints. The verifier below
//! reconstructs the trie root from returned leaves plus proof subtrees that are
//! outside the proven range.
use alloy_primitives::{Bytes, B256};
use alloy_rlp::Decodable;
use reth_trie::{HashBuilder, Nibbles, RlpNode, TrieNode, EMPTY_ROOT_HASH};
use std::collections::HashMap;
const KEY_NIBBLES: usize = 64;
const MAX_HASH: B256 = B256::new([0xff; 32]);
/// Error returned when a snap range proof is invalid.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub(crate) enum RangeProofError {
/// The response leaves were not strictly increasing by hashed key.
#[error("range leaves are not strictly increasing")]
NonMonotonicLeaves,
/// A returned leaf is before the requested range origin.
#[error("range leaf {key} is before origin {origin}")]
LeafBeforeOrigin {
/// The invalid leaf key.
key: B256,
/// Requested range origin.
origin: B256,
},
/// A proof node needed to reconstruct the trie boundary was missing.
#[error("missing proof node at path {path:?}")]
MissingProofNode {
/// Trie path whose node reference was required.
path: Nibbles,
},
/// A decoded proof path exceeded the fixed 32-byte hashed-key length.
#[error("proof path {path:?} exceeds hashed key length")]
PathTooLong {
/// Invalid trie path.
path: Nibbles,
},
/// A leaf proof path did not resolve to a full 32-byte hashed key.
#[error("leaf proof path {path:?} does not resolve to a full hashed key")]
InvalidLeafPath {
/// Invalid trie path.
path: Nibbles,
},
/// The reconstructed frontier contained duplicate paths.
#[error("range proof frontier contains duplicate path {path:?}")]
DuplicateFrontierPath {
/// Duplicate trie path.
path: Nibbles,
},
/// The reconstructed root does not match the expected trie root.
#[error("range proof root mismatch: expected {expected}, got {got}")]
RootMismatch {
/// Expected trie root.
expected: B256,
/// Root reconstructed from leaves and proof frontier.
got: B256,
},
/// A trie node failed to decode.
#[error(transparent)]
Rlp(#[from] alloy_rlp::Error),
}
/// Verifies that `leaves` are complete from `origin` through the last returned
/// leaf, or through the end of the trie when no leaves were returned.
pub(crate) fn verify_range_proof<I, V>(
root: B256,
origin: B256,
leaves: I,
proof: &[Bytes],
) -> Result<(), RangeProofError>
where
I: IntoIterator<Item = (B256, V)>,
V: AsRef<[u8]>,
{
let mut frontier = Vec::new();
let mut previous = None;
let mut last_key = None;
for (key, value) in leaves {
if key < origin {
return Err(RangeProofError::LeafBeforeOrigin { key, origin })
}
if previous.is_some_and(|previous| key <= previous) {
return Err(RangeProofError::NonMonotonicLeaves)
}
previous = Some(key);
last_key = Some(key);
frontier.push(FrontierEntry::Leaf {
path: Nibbles::unpack(key),
value: value.as_ref().to_vec(),
});
}
if root == EMPTY_ROOT_HASH {
if frontier.is_empty() {
return Ok(())
}
return Err(RangeProofError::RootMismatch { expected: root, got: frontier_root(frontier)? })
}
if !proof.is_empty() && !proof_is_empty_root(proof) {
let proof_by_reference = proof
.iter()
.map(|node| (RlpNode::from_rlp(node).as_slice().to_vec(), node.as_ref()))
.collect::<HashMap<_, _>>();
let left = Nibbles::unpack(origin);
let right = Nibbles::unpack(last_key.unwrap_or(MAX_HASH));
visit_reference(
Nibbles::new(),
&RlpNode::word_rlp(&root),
&left,
&right,
&proof_by_reference,
&mut frontier,
)?;
}
let got = frontier_root(frontier)?;
if got != root {
return Err(RangeProofError::RootMismatch { expected: root, got })
}
Ok(())
}
fn proof_is_empty_root(proof: &[Bytes]) -> bool {
proof.len() == 1 && proof[0].as_ref() == [alloy_rlp::EMPTY_STRING_CODE]
}
fn visit_reference(
prefix: Nibbles,
reference: &RlpNode,
left: &Nibbles,
right: &Nibbles,
proof_by_reference: &HashMap<Vec<u8>, &[u8]>,
frontier: &mut Vec<FrontierEntry>,
) -> Result<(), RangeProofError> {
match subtree_relation(&prefix, left, right)? {
SubtreeRelation::Outside => add_outside_reference(prefix, reference, frontier),
SubtreeRelation::Inside => Ok(()),
SubtreeRelation::Boundary => {
let node = resolve_reference(prefix, reference, proof_by_reference)?;
visit_node(node, prefix, left, right, proof_by_reference, frontier)
}
}
}
fn visit_node(
node: TrieNode,
prefix: Nibbles,
left: &Nibbles,
right: &Nibbles,
proof_by_reference: &HashMap<Vec<u8>, &[u8]>,
frontier: &mut Vec<FrontierEntry>,
) -> Result<(), RangeProofError> {
match node {
TrieNode::EmptyRoot => Ok(()),
TrieNode::Leaf(leaf) => {
let path = join_path(prefix, &leaf.key)?;
if path.len() != KEY_NIBBLES {
return Err(RangeProofError::InvalidLeafPath { path })
}
if key_in_range(&path, left, right) {
Ok(())
} else {
frontier.push(FrontierEntry::Leaf { path, value: leaf.value });
Ok(())
}
}
TrieNode::Extension(extension) => visit_reference(
join_path(prefix, &extension.key)?,
&extension.child,
left,
right,
proof_by_reference,
frontier,
),
TrieNode::Branch(branch) => {
for (nibble, child) in branch
.as_ref()
.children()
.filter_map(|(nibble, child)| child.map(|child| (nibble, child)))
{
let mut child_prefix = prefix;
child_prefix.push(nibble);
visit_reference(child_prefix, child, left, right, proof_by_reference, frontier)?;
}
Ok(())
}
}
}
fn add_outside_reference(
prefix: Nibbles,
reference: &RlpNode,
frontier: &mut Vec<FrontierEntry>,
) -> Result<(), RangeProofError> {
if prefix.len() > KEY_NIBBLES {
return Err(RangeProofError::PathTooLong { path: prefix })
}
if let Some(hash) = reference.as_hash() {
frontier.push(FrontierEntry::Subtree { path: prefix, hash });
return Ok(())
}
add_outside_node(TrieNode::decode(&mut reference.as_slice())?, prefix, frontier)
}
fn add_outside_node(
node: TrieNode,
prefix: Nibbles,
frontier: &mut Vec<FrontierEntry>,
) -> Result<(), RangeProofError> {
match node {
TrieNode::EmptyRoot => Ok(()),
TrieNode::Leaf(leaf) => {
let path = join_path(prefix, &leaf.key)?;
if path.len() != KEY_NIBBLES {
return Err(RangeProofError::InvalidLeafPath { path })
}
frontier.push(FrontierEntry::Leaf { path, value: leaf.value });
Ok(())
}
TrieNode::Extension(extension) => {
add_outside_reference(join_path(prefix, &extension.key)?, &extension.child, frontier)
}
TrieNode::Branch(branch) => {
for (nibble, child) in branch
.as_ref()
.children()
.filter_map(|(nibble, child)| child.map(|child| (nibble, child)))
{
let mut child_prefix = prefix;
child_prefix.push(nibble);
add_outside_reference(child_prefix, child, frontier)?;
}
Ok(())
}
}
}
fn resolve_reference(
path: Nibbles,
reference: &RlpNode,
proof_by_reference: &HashMap<Vec<u8>, &[u8]>,
) -> Result<TrieNode, RangeProofError> {
if !reference.is_hash() {
return Ok(TrieNode::decode(&mut reference.as_slice())?)
}
let Some(node) = proof_by_reference.get(reference.as_slice()) else {
return Err(RangeProofError::MissingProofNode { path })
};
Ok(TrieNode::decode(&mut &node[..])?)
}
fn join_path(mut prefix: Nibbles, suffix: &Nibbles) -> Result<Nibbles, RangeProofError> {
prefix.extend(suffix);
if prefix.len() > KEY_NIBBLES {
return Err(RangeProofError::PathTooLong { path: prefix })
}
Ok(prefix)
}
fn frontier_root(mut frontier: Vec<FrontierEntry>) -> Result<B256, RangeProofError> {
frontier.sort_unstable_by_key(|entry| entry.path());
let mut builder = HashBuilder::default();
let mut previous = None;
for entry in frontier {
let path = entry.path();
if previous.is_some_and(|previous| path <= previous) {
return Err(RangeProofError::DuplicateFrontierPath { path })
}
previous = Some(path);
match entry {
FrontierEntry::Leaf { path, value } => builder.add_leaf(path, &value),
FrontierEntry::Subtree { path, hash } => builder.add_branch(path, hash, false),
}
}
Ok(builder.root())
}
fn subtree_relation(
prefix: &Nibbles,
left: &Nibbles,
right: &Nibbles,
) -> Result<SubtreeRelation, RangeProofError> {
if prefix.len() > KEY_NIBBLES {
return Err(RangeProofError::PathTooLong { path: *prefix })
}
let min = padded_path(prefix, 0);
let max = padded_path(prefix, 0x0f);
let left = padded_path(left, 0);
let right = padded_path(right, 0x0f);
if max < left || min > right {
Ok(SubtreeRelation::Outside)
} else if min >= left && max <= right {
Ok(SubtreeRelation::Inside)
} else {
Ok(SubtreeRelation::Boundary)
}
}
fn key_in_range(key: &Nibbles, left: &Nibbles, right: &Nibbles) -> bool {
let key = padded_path(key, 0);
key >= padded_path(left, 0) && key <= padded_path(right, 0x0f)
}
fn padded_path(path: &Nibbles, fill: u8) -> [u8; KEY_NIBBLES] {
let mut padded = [fill; KEY_NIBBLES];
for (idx, nibble) in padded.iter_mut().enumerate().take(path.len()) {
*nibble = path.get(idx).expect("idx is below path length");
}
padded
}
#[derive(Clone, Debug)]
enum FrontierEntry {
Leaf { path: Nibbles, value: Vec<u8> },
Subtree { path: Nibbles, hash: B256 },
}
impl FrontierEntry {
const fn path(&self) -> Nibbles {
match self {
Self::Leaf { path, .. } | Self::Subtree { path, .. } => *path,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SubtreeRelation {
Outside,
Boundary,
Inside,
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_trie::proof::ProofRetainer;
use reth_trie::HashBuilder;
fn b256(value: u64) -> B256 {
B256::left_padding_from(&value.to_be_bytes())
}
fn value(byte: u8) -> Vec<u8> {
vec![byte; 64]
}
fn build_proof(leaves: &[(B256, Vec<u8>)], targets: &[B256]) -> (B256, Vec<Bytes>) {
let targets = targets.iter().copied().map(Nibbles::unpack).collect();
let mut builder = HashBuilder::default().with_proof_retainer(ProofRetainer::new(targets));
for (key, value) in leaves {
builder.add_leaf(Nibbles::unpack(*key), value);
}
let root = builder.root();
let proof = builder
.take_proof_nodes()
.into_nodes_sorted()
.into_iter()
.map(|(_, node)| node)
.collect();
(root, proof)
}
#[test]
fn complete_range_accepts_boundary_multiproof() {
let leaves = vec![
(b256(1), value(1)),
(b256(2), value(2)),
(b256(3), value(3)),
(b256(4), value(4)),
];
let returned = leaves[1..=3].to_vec();
let (root, proof) = build_proof(&leaves, &[b256(2), b256(4)]);
verify_range_proof(root, b256(2), returned, &proof).unwrap();
}
#[test]
fn proof_free_full_range_verifies_from_leaves() {
let leaves = vec![(b256(1), value(1)), (b256(2), value(2)), (b256(3), value(3))];
let (root, _) = build_proof(&leaves, &[]);
verify_range_proof(root, B256::ZERO, leaves, &[]).unwrap();
}
#[test]
fn range_rejects_omitted_interior_leaf() {
let leaves = vec![
(b256(1), value(1)),
(b256(2), value(2)),
(b256(3), value(3)),
(b256(4), value(4)),
];
let returned = vec![(b256(2), value(2)), (b256(4), value(4))];
let (root, proof) = build_proof(&leaves, &[b256(2), b256(4)]);
assert!(matches!(
verify_range_proof(root, b256(2), returned, &proof),
Err(RangeProofError::RootMismatch { .. })
));
}
#[test]
fn empty_tail_range_accepts_absence_proof() {
let leaves = vec![(b256(1), value(1)), (b256(2), value(2))];
let (root, proof) = build_proof(&leaves, &[b256(3)]);
verify_range_proof(root, b256(3), std::iter::empty::<(B256, Vec<u8>)>(), &proof).unwrap();
}
#[test]
fn empty_range_rejects_omitted_right_leaf() {
let leaves = vec![(b256(1), value(1)), (b256(3), value(3))];
let (root, proof) = build_proof(&leaves, &[b256(2)]);
assert!(matches!(
verify_range_proof(root, b256(2), std::iter::empty::<(B256, Vec<u8>)>(), &proof),
Err(RangeProofError::RootMismatch { .. })
));
}
}

View File

@@ -0,0 +1,481 @@
//! Snap protocol state provider backed by a chain-aware provider.
//!
//! Serves recent historical state for request roots by applying a revert overlay
//! on top of the current MDBX hashed state. This keeps the served state fully
//! persisted and deterministic.
use alloy_consensus::{BlockHeader, EMPTY_ROOT_HASH};
use alloy_primitives::{map::B256Map, Bytes, B256};
use reth_db_api::transaction::DbTx;
use reth_eth_wire_types::{
snap::{AccountData, StorageData},
BlockAccessLists,
};
use reth_network_p2p::snap::server::SnapStateProvider;
use reth_provider::LatestStateProviderRef;
use reth_stages_types::StageId;
use reth_storage_api::{
BalProvider, BlockHashReader, BlockNumReader, BytecodeReader, ChangeSetReader, DBProvider,
DatabaseProviderFactory, HeaderProvider, StageCheckpointReader, StorageChangeSetReader,
StorageSettingsCache,
};
use reth_trie::{
hashed_cursor::{HashedCursor, HashedCursorFactory, HashedPostStateCursorFactory},
prefix_set::PrefixSetMut,
proof::{Proof, StorageProof},
HashedPostStateSorted, HashedStorageSorted, MultiProofTargets, Nibbles,
};
use reth_trie_db::{
DatabaseHashedCursorFactory, DatabaseHashedPostState, DatabaseTrieCursorFactory,
};
use std::sync::atomic::{AtomicU64, Ordering};
/// Maximum accounts to return per snap request.
const MAX_ACCOUNTS_SERVE: usize = 4096;
/// Default maximum number of recent blocks to scan when resolving a root hash.
const DEFAULT_MAX_SERVING_LOOKBACK: u64 = 128;
/// Maximum number of recent blocks to scan when resolving a root hash.
static MAX_SERVING_LOOKBACK: AtomicU64 = AtomicU64::new(DEFAULT_MAX_SERVING_LOOKBACK);
/// Snap state provider that wraps a chain-aware provider and serves historical
/// state via a revert overlay.
///
/// The provider `P` must implement [`BlockNumReader`] and [`HeaderProvider`]
/// directly so request roots can be resolved against the canonical in-memory
/// tip, not just static-file-persisted blocks. In practice, pass a
/// `BlockchainProvider`.
pub struct ProviderSnapState<P> {
provider: P,
}
impl<P> ProviderSnapState<P> {
/// Create a new snap state provider.
pub const fn new(provider: P) -> Self {
Self { provider }
}
}
impl<P> core::fmt::Debug for ProviderSnapState<P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ProviderSnapState").finish()
}
}
/// Guard that restores the previous snap serving lookback when dropped.
#[cfg(any(test, feature = "test-utils"))]
#[derive(Debug)]
pub struct MaxServingLookbackGuard {
previous: u64,
}
#[cfg(any(test, feature = "test-utils"))]
impl Drop for MaxServingLookbackGuard {
fn drop(&mut self) {
MAX_SERVING_LOOKBACK.store(self.previous, Ordering::Relaxed);
}
}
/// Overrides the serving lookback for tests.
#[cfg(any(test, feature = "test-utils"))]
pub fn set_max_serving_lookback_for_tests(lookback: u64) -> MaxServingLookbackGuard {
let previous = MAX_SERVING_LOOKBACK.swap(lookback, Ordering::Relaxed);
MaxServingLookbackGuard { previous }
}
impl<P> ProviderSnapState<P>
where
P: HeaderProvider + BlockNumReader,
{
/// Scan recent headers for one whose state root matches `root_hash` and
/// return its block number.
fn resolve_serving_block(&self, root_hash: B256) -> Option<u64> {
let tip = self.provider.best_block_number().ok()?;
let start = tip.saturating_sub(MAX_SERVING_LOOKBACK.load(Ordering::Relaxed));
for num in (start..=tip).rev() {
if let Ok(Some(header)) = self.provider.header_by_number(num) {
if header.state_root() == root_hash {
return Some(num);
}
}
}
None
}
}
impl<P> ProviderSnapState<P>
where
P: DatabaseProviderFactory + HeaderProvider + BlockNumReader,
P::Provider: DBProvider
+ StageCheckpointReader
+ ChangeSetReader
+ StorageChangeSetReader
+ BlockHashReader
+ BlockNumReader
+ StorageSettingsCache,
<P::Provider as DBProvider>::Tx: DbTx,
{
fn database_provider_with_reverts(
&self,
root_hash: B256,
) -> Option<(P::Provider, HashedPostStateSorted)> {
let serving_block = self.resolve_serving_block(root_hash)?;
let provider = self.provider.database_provider_ro().ok()?;
let persisted = provider.get_stage_checkpoint(StageId::Execution).ok()??.block_number;
let revert_state = if persisted > serving_block {
HashedPostStateSorted::from_reverts(&provider, (serving_block + 1)..=persisted).ok()?
} else {
HashedPostStateSorted::default()
};
Some((provider, revert_state))
}
}
impl<P> SnapStateProvider for ProviderSnapState<P>
where
P: DatabaseProviderFactory
+ HeaderProvider
+ BlockNumReader
+ BalProvider
+ Send
+ Sync
+ 'static,
P::Provider: DBProvider
+ StageCheckpointReader
+ ChangeSetReader
+ StorageChangeSetReader
+ BlockNumReader
+ StorageSettingsCache,
<P::Provider as DBProvider>::Tx: DbTx,
{
fn account_range(
&self,
root_hash: B256,
starting_hash: B256,
limit_hash: B256,
response_bytes: u64,
) -> (Vec<AccountData>, Vec<Bytes>) {
let empty = (Vec::new(), Vec::new());
let Some((provider, revert_state)) = self.database_provider_with_reverts(root_hash) else {
return empty;
};
let cursor_factory = HashedPostStateCursorFactory::new(
DatabaseHashedCursorFactory::new(provider.tx_ref()),
&revert_state,
);
let Ok(mut cursor) = cursor_factory.hashed_account_cursor() else { return empty };
let mut raw_accounts = Vec::new();
let mut total_bytes: u64 = 0;
if let Ok(Some((hash, account))) = cursor.seek(starting_hash) {
let body_len = snap_account_body_len_upper_bound(account);
total_bytes += body_len as u64 + 32;
raw_accounts.push((hash, account));
if hash < limit_hash {
while raw_accounts.len() < MAX_ACCOUNTS_SERVE && total_bytes < response_bytes {
match cursor.next() {
Ok(Some((hash, account))) if hash < limit_hash => {
let body_len = snap_account_body_len_upper_bound(account);
total_bytes += body_len as u64 + 32;
raw_accounts.push((hash, account));
}
_ => break,
}
}
}
}
let Some((storage_roots, proof)) =
account_range_proof(&provider, &revert_state, starting_hash, &raw_accounts)
else {
return empty;
};
let accounts = raw_accounts
.into_iter()
.map(|(hash, account)| {
let storage_root = storage_roots.get(&hash).copied().unwrap_or(EMPTY_ROOT_HASH);
AccountData { hash, account: account.into_trie_account(storage_root) }
})
.collect();
(accounts, proof)
}
fn storage_ranges(
&self,
root_hash: B256,
account_hashes: Vec<B256>,
starting_hash: B256,
limit_hash: B256,
response_bytes: u64,
) -> (Vec<Vec<StorageData>>, Vec<Bytes>) {
let empty = (Vec::new(), Vec::new());
let Some((provider, revert_state)) = self.database_provider_with_reverts(root_hash) else {
return empty;
};
let cursor_factory = HashedPostStateCursorFactory::new(
DatabaseHashedCursorFactory::new(provider.tx_ref()),
&revert_state,
);
if !requested_accounts_available(&cursor_factory, &account_hashes).unwrap_or(false) {
return empty;
}
let mut all_slots: Vec<Vec<StorageData>> = Vec::new();
let mut total_bytes: u64 = 0;
let mut partial_range = None;
for (i, account_hash) in account_hashes.iter().enumerate() {
let prior_slots_returned = all_slots.iter().any(|slots| !slots.is_empty());
if total_bytes >= response_bytes && prior_slots_returned {
break;
}
let mut slots = Vec::new();
let start = if i == 0 { starting_hash } else { B256::ZERO };
let Ok(mut cursor) = cursor_factory.hashed_storage_cursor(*account_hash) else {
all_slots.push(slots);
continue;
};
if let Ok(Some((key, value))) = cursor.seek(start) &&
key < limit_hash &&
!value.is_zero()
{
let slot = StorageData::from_value(key, value);
total_bytes += slot.data.len() as u64 + 32;
slots.push(slot);
}
while total_bytes < response_bytes || (!prior_slots_returned && slots.is_empty()) {
match cursor.next() {
Ok(Some((key, value))) if key < limit_hash => {
if value.is_zero() {
continue;
}
let slot = StorageData::from_value(key, value);
total_bytes += slot.data.len() as u64 + 32;
slots.push(slot);
}
_ => break,
}
}
if total_bytes >= response_bytes &&
storage_has_more_slots(&mut cursor, limit_hash).unwrap_or(false)
{
partial_range = Some((*account_hash, start, slots.last().map(|slot| slot.hash)));
}
all_slots.push(slots);
if partial_range.is_some() {
break;
}
}
let proof = partial_range
.and_then(|(account_hash, start, last_hash)| {
storage_range_proof(&provider, &revert_state, account_hash, start, last_hash)
})
.unwrap_or_default();
(all_slots, proof)
}
fn bytecodes(&self, hashes: Vec<B256>, response_bytes: u64) -> Vec<Bytes> {
let Ok(provider) = self.provider.database_provider_ro() else {
return Vec::new();
};
let mut out = Vec::new();
let mut total: u64 = 0;
let state = LatestStateProviderRef::new(&provider);
for hash in hashes {
if total >= response_bytes {
break;
}
if let Ok(Some(code)) = state.bytecode_by_hash(&hash) {
let bytes = code.original_bytes();
total += bytes.len() as u64;
out.push(bytes);
}
}
out
}
fn block_access_lists(&self, block_hashes: Vec<B256>, response_bytes: u64) -> BlockAccessLists {
serve_block_access_lists(&self.provider, block_hashes, response_bytes)
}
}
fn account_range_proof<Provider>(
provider: &Provider,
revert_state: &HashedPostStateSorted,
starting_hash: B256,
accounts: &[(B256, reth_primitives_traits::Account)],
) -> Option<(B256Map<B256>, Vec<Bytes>)>
where
Provider: DBProvider + StorageSettingsCache,
Provider::Tx: DbTx,
{
reth_trie_db::with_adapter!(provider, |A| {
let mut targets = MultiProofTargets::accounts([starting_hash]);
targets.extend(MultiProofTargets::accounts(accounts.iter().map(|(hash, _)| *hash)));
let multiproof = Proof::new(
DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()),
HashedPostStateCursorFactory::new(
DatabaseHashedCursorFactory::new(provider.tx_ref()),
revert_state,
),
)
.with_prefix_sets_mut(revert_state.construct_prefix_sets())
.multiproof(targets)
.ok()?;
let storage_roots =
multiproof.storages.iter().map(|(hash, proof)| (*hash, proof.root)).collect();
let proof = multiproof
.account_subtree
.into_nodes_sorted()
.into_iter()
.map(|(_, node)| node)
.collect();
Some((storage_roots, proof))
})
}
fn requested_accounts_available<CF>(
cursor_factory: &CF,
account_hashes: &[B256],
) -> Result<bool, reth_db_api::DatabaseError>
where
CF: HashedCursorFactory,
{
let mut cursor = cursor_factory.hashed_account_cursor()?;
for account_hash in account_hashes {
if !matches!(cursor.seek(*account_hash)?, Some((hash, _)) if hash == *account_hash) {
return Ok(false);
}
cursor.reset();
}
Ok(true)
}
fn snap_account_body_len_upper_bound(account: reth_primitives_traits::Account) -> usize {
AccountData::account_body_len(account.into_trie_account(B256::ZERO))
}
fn storage_range_proof<Provider>(
provider: &Provider,
revert_state: &HashedPostStateSorted,
account_hash: B256,
starting_hash: B256,
last_hash: Option<B256>,
) -> Option<Vec<Bytes>>
where
Provider: DBProvider + StorageSettingsCache,
Provider::Tx: DbTx,
{
reth_trie_db::with_adapter!(provider, |A| {
let targets = last_hash.map_or_else(
|| alloy_primitives::map::B256Set::from_iter([starting_hash]),
|last_hash| alloy_primitives::map::B256Set::from_iter([starting_hash, last_hash]),
);
let multiproof = StorageProof::new_hashed(
DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref()),
HashedPostStateCursorFactory::new(
DatabaseHashedCursorFactory::new(provider.tx_ref()),
revert_state,
),
account_hash,
)
.with_prefix_set_mut(storage_prefix_set_mut(revert_state, account_hash))
.storage_multiproof(targets)
.ok()?;
Some(multiproof.subtree.into_nodes_sorted().into_iter().map(|(_, node)| node).collect())
})
}
fn storage_prefix_set_mut(
revert_state: &HashedPostStateSorted,
account_hash: B256,
) -> PrefixSetMut {
match revert_state.account_storages().get(&account_hash) {
Some(storage) => storage_prefix_set_from_sorted(storage),
None => PrefixSetMut::default(),
}
}
fn storage_prefix_set_from_sorted(storage: &HashedStorageSorted) -> PrefixSetMut {
if storage.wiped {
return PrefixSetMut::all();
}
let mut prefix_set = PrefixSetMut::with_capacity(storage.storage_slots.len());
prefix_set.extend_keys(storage.storage_slots.iter().map(|(slot, _)| Nibbles::unpack(slot)));
prefix_set
}
fn storage_has_more_slots<C>(
cursor: &mut C,
limit_hash: B256,
) -> Result<bool, reth_db_api::DatabaseError>
where
C: HashedCursor<Value = alloy_primitives::U256>,
{
while let Some((key, value)) = cursor.next()? {
if key >= limit_hash {
return Ok(false);
}
if !value.is_zero() {
return Ok(true);
}
}
Ok(false)
}
fn serve_block_access_lists<P>(
provider: &P,
block_hashes: Vec<B256>,
response_bytes: u64,
) -> BlockAccessLists
where
P: BalProvider,
{
let results = match provider.bal_store().get_by_hashes(&block_hashes) {
Ok(results) => results,
Err(_) => return BlockAccessLists(Vec::new()),
};
let mut total_bytes = 0u64;
let mut out = Vec::new();
for bal in results {
let bal = bal.unwrap_or_else(|| Bytes::from_static(&[alloy_rlp::EMPTY_STRING_CODE]));
total_bytes += bal.len() as u64;
out.push(bal);
if total_bytes >= response_bytes {
break;
}
}
BlockAccessLists(out)
}

View File

@@ -0,0 +1,184 @@
//! MDBX read/write helpers for hashed state and bytecodes.
use crate::SnapSyncError;
use alloy_primitives::{map::B256Map, Bytes, B256, U256};
use reth_db_api::{
tables,
transaction::{DbTx, DbTxMut},
};
use reth_primitives_traits::{Account, Bytecode};
use reth_provider::DatabaseProviderFactory;
use reth_storage_api::{DBProvider, StateWriter};
use reth_trie::{HashedPostStateSorted, HashedStorageSorted};
// ──────────────────────────────────────────────────────────────────────────────
// MDBX write helpers
// ──────────────────────────────────────────────────────────────────────────────
/// Clears all hashed state tables.
pub(crate) fn clear_hashed_state<F>(factory: &F) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let provider = factory.database_provider_rw().map_err(db_err)?;
{
let tx = provider.tx_ref();
tx.clear::<tables::HashedAccounts>().map_err(db_err)?;
tx.clear::<tables::HashedStorages>().map_err(db_err)?;
tx.clear::<tables::AccountsTrie>().map_err(db_err)?;
tx.clear::<tables::StoragesTrie>().map_err(db_err)?;
tx.clear::<tables::Bytecodes>().map_err(db_err)?;
}
provider.commit().map_err(db_err)?;
Ok(())
}
/// Reads a single hashed account from the database.
pub(crate) fn read_hashed_account<F>(
factory: &F,
hashed_address: B256,
) -> Result<Option<Account>, SnapSyncError>
where
F: DatabaseProviderFactory,
F::Provider: DBProvider,
<F::Provider as DBProvider>::Tx: DbTx,
{
let provider = factory.database_provider_ro().map_err(db_err)?;
let tx = provider.tx_ref();
tx.get::<tables::HashedAccounts>(hashed_address).map_err(db_err)
}
/// Writes a batch of hashed accounts.
pub(crate) fn write_hashed_accounts<F>(
factory: &F,
accounts: &[(B256, Account)],
) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let mut accounts_by_hash = B256Map::default();
for (hash, account) in accounts {
accounts_by_hash.insert(*hash, Some(*account));
}
let mut accounts: Vec<_> = accounts_by_hash.into_iter().collect();
accounts.sort_by_key(|(hash, _)| *hash);
write_hashed_state(factory, HashedPostStateSorted::new(accounts, B256Map::default()))
}
/// Writes a batch of hashed storage entries.
pub(crate) fn write_hashed_storages<F>(
factory: &F,
entries: &[(B256, B256, U256)],
) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let mut slots_by_account: B256Map<B256Map<U256>> = B256Map::default();
for &(account_hash, slot_hash, value) in entries {
slots_by_account.entry(account_hash).or_default().insert(slot_hash, value);
}
let storages = slots_by_account
.into_iter()
.map(|(account_hash, slots)| {
let mut storage_slots: Vec<_> = slots.into_iter().collect();
storage_slots.sort_by_key(|(slot_hash, _)| *slot_hash);
(account_hash, HashedStorageSorted { storage_slots, wiped: false })
})
.collect();
write_hashed_state(factory, HashedPostStateSorted::new(Vec::new(), storages))
}
fn write_hashed_state<F>(
factory: &F,
hashed_state: HashedPostStateSorted,
) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let provider = factory.database_provider_rw().map_err(db_err)?;
provider.write_hashed_state(&hashed_state).map_err(db_err)?;
provider.commit().map_err(db_err)?;
Ok(())
}
/// Writes a batch of bytecodes.
pub(crate) fn write_bytecodes<F>(factory: &F, codes: &[(B256, Bytes)]) -> Result<(), SnapSyncError>
where
F: DatabaseProviderFactory,
F::ProviderRW: DBProvider + StateWriter,
<F::ProviderRW as DBProvider>::Tx: DbTxMut,
{
let provider = factory.database_provider_rw().map_err(db_err)?;
provider
.write_bytecodes(
codes
.iter()
.filter(|(_, code)| !code.is_empty())
.map(|(hash, code)| (*hash, Bytecode::new_raw(code.clone()))),
)
.map_err(db_err)?;
provider.commit().map_err(db_err)?;
Ok(())
}
/// Increment a [`B256`] by 1 for pagination.
pub(crate) fn increment_b256(hash: B256) -> B256 {
let mut bytes = hash.0;
for byte in bytes.iter_mut().rev() {
if *byte == 0xff {
*byte = 0;
} else {
*byte += 1;
return B256::from(bytes);
}
}
B256::ZERO
}
pub(crate) fn db_err(e: impl std::error::Error + Send + Sync + 'static) -> SnapSyncError {
SnapSyncError::Database(e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_increment_b256_simple() {
let hash = B256::ZERO;
let next = increment_b256(hash);
let mut expected = [0u8; 32];
expected[31] = 1;
assert_eq!(next, B256::from(expected));
}
#[test]
fn test_increment_b256_carry() {
let mut bytes = [0u8; 32];
bytes[31] = 0xff;
let hash = B256::from(bytes);
let next = increment_b256(hash);
let mut expected = [0u8; 32];
expected[30] = 1;
assert_eq!(next, B256::from(expected));
}
#[test]
fn test_increment_b256_max() {
let hash = B256::from([0xff; 32]);
let next = increment_b256(hash);
assert_eq!(next, B256::ZERO);
}
}

View File

@@ -17,6 +17,7 @@ reth-chainspec = { workspace = true, optional = true }
reth-consensus.workspace = true
reth-db.workspace = true
reth-engine-primitives = { workspace = true, features = ["std"] }
reth-engine-snap.workspace = true
reth-execution-cache.workspace = true
reth-errors.workspace = true
reth-execution-types.workspace = true
@@ -29,6 +30,7 @@ reth-ethereum-primitives.workspace = true
reth-provider.workspace = true
reth-prune.workspace = true
reth-revm = { workspace = true, features = ["optional-balance-check"] }
reth-stages = { workspace = true, optional = true }
reth-stages-api.workspace = true
reth-tasks = { workspace = true, features = ["rayon"] }
reth-trie-parallel.workspace = true
@@ -70,7 +72,6 @@ crossbeam-channel.workspace = true
# optional deps for test-utils
reth-prune-types = { workspace = true, optional = true }
reth-stages = { workspace = true, optional = true }
reth-static-file = { workspace = true, optional = true }
reth-tracing = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
@@ -119,6 +120,7 @@ test-utils = [
"reth-prune-types",
"reth-prune-types?/test-utils",
"reth-revm/test-utils",
"reth-stages",
"reth-stages-api/test-utils",
"reth-stages/test-utils",
"reth-static-file",
@@ -134,6 +136,7 @@ test-utils = [
"reth-evm-ethereum/test-utils",
"reth-tasks/test-utils",
"reth-execution-cache/test-utils",
"reth-engine-snap/test-utils",
]
trie-debug = [
"reth-trie-sparse/trie-debug",

View File

@@ -8,10 +8,14 @@
//! These modes are mutually exclusive and the node can only be in one mode at a time.
use futures::FutureExt;
use reth_provider::providers::ProviderNodeTypes;
use reth_engine_snap::controller::{SnapSyncControl, SnapSyncControlEvent, SnapSyncController};
use reth_provider::{providers::ProviderNodeTypes, ProviderFactory};
use reth_stages_api::{ControlFlow, Pipeline, PipelineError, PipelineTarget, PipelineWithResult};
use reth_tasks::Runtime;
use std::task::{ready, Context, Poll};
use std::{
fmt,
task::{ready, Context, Poll},
};
use tokio::sync::oneshot;
use tracing::trace;
@@ -60,6 +64,11 @@ pub trait BackfillSync: Send {
pub enum BackfillAction {
/// Start backfilling with the given target.
Start(PipelineTarget),
/// Start snap sync (for fresh nodes with no state).
///
/// Carries the target block hash from the FCU so the orchestrator can resolve
/// the head from peers if it's not available locally.
StartSnapSync(alloy_primitives::B256),
}
/// The events that can be emitted on backfill sync.
@@ -74,6 +83,10 @@ pub enum BackfillEvent {
/// Sync task was dropped after it was started, unable to receive it because
/// channel closed. This would indicate a panicked task.
TaskDropped(String),
/// Snap sync started. Contains the event sender for forwarding chain events.
SnapSyncStarted(tokio::sync::mpsc::UnboundedSender<reth_engine_snap::SnapSyncEvent>),
/// Snap sync finished.
SnapSyncFinished(Result<reth_engine_snap::SnapSyncOutcome, reth_engine_snap::SnapSyncError>),
}
/// Pipeline sync.
@@ -110,7 +123,7 @@ impl<N: ProviderNodeTypes> PipelineSync<N> {
}
/// Returns `true` if the pipeline is active.
const fn is_pipeline_active(&self) -> bool {
pub(crate) const fn is_pipeline_active(&self) -> bool {
!self.is_pipeline_idle()
}
@@ -181,6 +194,7 @@ impl<N: ProviderNodeTypes> BackfillSync for PipelineSync<N> {
fn on_action(&mut self, event: BackfillAction) {
match event {
BackfillAction::Start(target) => self.set_pipeline_sync_target(target),
BackfillAction::StartSnapSync(_) => {}
}
}
@@ -226,6 +240,111 @@ impl<N: ProviderNodeTypes> PipelineState<N> {
}
}
/// Combined backfill sync that supports both pipeline sync and snap sync.
///
/// Only one sync mode can be active at a time.
pub struct CombinedBackfillSync<N: ProviderNodeTypes, S> {
pipeline: PipelineSync<N>,
snap: S,
pending_event: Option<BackfillEvent>,
}
/// Combined backfill sync using the default provider factory snap controller.
pub type EngineBackfillSync<N, C> =
CombinedBackfillSync<N, SnapSyncController<C, ProviderFactory<N>>>;
impl<N, S> fmt::Debug for CombinedBackfillSync<N, S>
where
N: ProviderNodeTypes,
S: SnapSyncControl,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CombinedBackfillSync")
.field("pipeline_active", &self.pipeline.is_pipeline_active())
.field("snap_active", &self.snap.is_active())
.field("pending_event", &self.pending_event)
.finish()
}
}
impl<N, S> CombinedBackfillSync<N, S>
where
N: ProviderNodeTypes,
S: SnapSyncControl,
{
/// Creates a new combined backfill sync with the given snap sync adapter.
pub fn with_snap(pipeline: PipelineSync<N>, snap: S) -> Self {
Self { pipeline, snap, pending_event: None }
}
}
impl<N, C, F> CombinedBackfillSync<N, SnapSyncController<C, F>>
where
N: ProviderNodeTypes,
SnapSyncController<C, F>: SnapSyncControl,
{
/// Creates a new combined backfill sync.
pub fn new(
pipeline: Pipeline<N>,
pipeline_task_spawner: Runtime,
client: C,
factory: F,
snap_runtime: Runtime,
) -> Self {
Self::with_snap(
PipelineSync::new(pipeline, pipeline_task_spawner),
SnapSyncController::new(client, factory, snap_runtime),
)
}
}
impl<N, S> BackfillSync for CombinedBackfillSync<N, S>
where
N: ProviderNodeTypes,
S: SnapSyncControl,
{
fn on_action(&mut self, action: BackfillAction) {
match action {
BackfillAction::Start(target) => {
if self.snap.is_active() {
tracing::warn!(target: "consensus::engine::sync", "Ignoring pipeline start while snap sync is active");
return;
}
self.pipeline.on_action(BackfillAction::Start(target));
}
BackfillAction::StartSnapSync(target_hash) => {
if self.pipeline.is_pipeline_active() {
tracing::warn!(target: "consensus::engine::sync", "Ignoring snap sync start while pipeline is active");
return;
}
self.snap.start(target_hash);
self.pending_event = None;
}
}
}
fn poll(&mut self, cx: &mut Context<'_>) -> Poll<BackfillEvent> {
// Return any pending event first (e.g. snap sync started)
if let Some(event) = self.pending_event.take() {
return Poll::Ready(event);
}
// Poll snap sync
if let Poll::Ready(event) = self.snap.poll(cx) {
return Poll::Ready(match event {
SnapSyncControlEvent::Started(events_tx) => {
BackfillEvent::SnapSyncStarted(events_tx)
}
SnapSyncControlEvent::Finished(result) => BackfillEvent::SnapSyncFinished(result),
SnapSyncControlEvent::TaskDropped(err) => BackfillEvent::TaskDropped(err),
});
}
// Poll pipeline
self.pipeline.poll(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -106,6 +106,23 @@ where
tracing::error!( %err, "backfill sync task dropped");
return Poll::Ready(ChainEvent::FatalError);
}
BackfillEvent::SnapSyncStarted(events_tx) => {
this.handler.on_event(FromOrchestrator::SnapSyncStarted(events_tx));
return Poll::Ready(ChainEvent::SnapSyncStarted);
}
BackfillEvent::SnapSyncFinished(result) => {
return match result {
Ok(outcome) => {
tracing::info!(?outcome, "snap sync finished");
this.handler.on_event(FromOrchestrator::SnapSyncFinished(outcome));
Poll::Ready(ChainEvent::SnapSyncFinished)
}
Err(err) => {
tracing::error!(%err, "snap sync failed");
Poll::Ready(ChainEvent::FatalError)
}
}
}
},
Poll::Pending => {}
}
@@ -160,6 +177,10 @@ pub enum ChainEvent<T> {
BackfillSyncStarted,
/// Backfill sync finished
BackfillSyncFinished,
/// Snap sync started
SnapSyncStarted,
/// Snap sync finished
SnapSyncFinished,
/// Fatal error
FatalError,
/// Event emitted by the handler
@@ -175,6 +196,12 @@ impl<T: Display> Display for ChainEvent<T> {
Self::BackfillSyncFinished => {
write!(f, "BackfillSyncFinished")
}
Self::SnapSyncStarted => {
write!(f, "SnapSyncStarted")
}
Self::SnapSyncFinished => {
write!(f, "SnapSyncFinished")
}
Self::FatalError => {
write!(f, "FatalError")
}
@@ -225,6 +252,10 @@ pub enum FromOrchestrator {
BackfillSyncFinished(ControlFlow),
/// Invoked when backfill sync started
BackfillSyncStarted,
/// Invoked when snap sync started, carries the event sender for forwarding chain events.
SnapSyncStarted(tokio::sync::mpsc::UnboundedSender<reth_engine_snap::SnapSyncEvent>),
/// Invoked when snap sync finished.
SnapSyncFinished(reth_engine_snap::SnapSyncOutcome),
/// Gracefully terminate the engine service.
///
/// When this variant is received, the engine will persist all remaining in-memory blocks

View File

@@ -5,7 +5,7 @@
//! [`ChainOrchestrator`](crate::chain::ChainOrchestrator) ready to be polled as a `Stream`.
use crate::{
backfill::PipelineSync,
backfill::EngineBackfillSync,
chain::ChainOrchestrator,
download::BasicBlockDownloader,
engine::{EngineApiKind, EngineApiRequest, EngineApiRequestHandler, EngineHandler},
@@ -71,15 +71,21 @@ pub fn build_engine_orchestrator<N, Client, S, V, C>(
S,
BasicBlockDownloader<Client, <N::Primitives as NodePrimitives>::Block>,
>,
PipelineSync<N>,
EngineBackfillSync<N, Client>,
>
where
N: ProviderNodeTypes,
Client: BlockClient<Block = <N::Primitives as NodePrimitives>::Block> + 'static,
Client: BlockClient<Block = <N::Primitives as NodePrimitives>::Block>
+ reth_network_p2p::snap::client::SnapClient
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient
+ Clone
+ 'static,
S: Stream<Item = BeaconEngineMessage<N::Payload>> + Send + Sync + Unpin + 'static,
V: EngineValidator<N::Payload> + WaitForCaches,
C: ConfigureEvm<Primitives = N::Primitives> + 'static,
{
let snap_client = client.clone();
let snap_provider = provider.clone();
let downloader = BasicBlockDownloader::new(client, consensus.clone());
let persistence_handle =
@@ -87,6 +93,7 @@ where
let canonical_in_memory_state = blockchain_db.canonical_in_memory_state();
let snap_runtime = runtime.clone();
let (to_tree_tx, from_tree) = EngineApiTreeHandler::spawn_new(
blockchain_db,
consensus,
@@ -104,7 +111,13 @@ where
let engine_handler = EngineApiRequestHandler::new(to_tree_tx, from_tree);
let handler = EngineHandler::new(engine_handler, downloader, incoming_requests);
let backfill_sync = PipelineSync::new(pipeline, pipeline_task_spawner);
let backfill_sync = EngineBackfillSync::new(
pipeline,
pipeline_task_spawner,
snap_client,
snap_provider,
snap_runtime,
);
ChainOrchestrator::new(handler, backfill_sync)
}

View File

@@ -29,7 +29,7 @@ use reth_primitives_traits::{
FastInstant as Instant, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
};
use reth_provider::{
BlockExecutionOutput, BlockExecutionResult, BlockReader, ChangeSetReader,
BalProvider, BlockExecutionOutput, BlockExecutionResult, BlockReader, ChangeSetReader,
DatabaseProviderFactory, HashedPostStateProvider, ProviderError, StageCheckpointReader,
StateProviderBox, StateProviderFactory, StateReader, StorageChangeSetReader,
StorageSettingsCache, TransactionVariant,
@@ -58,6 +58,7 @@ pub mod payload_processor;
pub mod payload_validator;
mod persistence_state;
pub mod precompile_cache;
mod snap;
#[cfg(test)]
mod tests;
mod trie_updates;
@@ -314,6 +315,8 @@ where
building_payload: bool,
/// Task runtime for spawning blocking work on named, reusable threads.
runtime: reth_tasks::Runtime,
/// Snap sync state and event forwarding.
snap: snap::SnapTreeState,
}
impl<N, P: Debug, T: PayloadTypes + Debug, V: Debug, C> std::fmt::Debug
@@ -341,6 +344,7 @@ where
.field("changeset_cache", &self.changeset_cache)
.field("execution_timing_stats", &self.execution_timing_stats.len())
.field("runtime", &self.runtime)
.field("snap", &self.snap)
.finish()
}
}
@@ -353,6 +357,7 @@ where
+ StateProviderFactory
+ StateReader<Receipt = N::Receipt>
+ HashedPostStateProvider
+ BalProvider
+ Clone
+ 'static,
P::Provider: BlockReader<Block = N::Block, Header = N::BlockHeader>
@@ -405,6 +410,7 @@ where
execution_timing_stats: HashMap::new(),
building_payload: false,
runtime,
snap: snap::SnapTreeState::new(false),
}
}
@@ -445,7 +451,9 @@ where
kind,
);
let task = Self::new(
let fresh_node = best_block_number == 0;
let mut task = Self::new(
provider,
consensus,
payload_validator,
@@ -461,6 +469,8 @@ where
changeset_cache,
runtime,
);
task.snap.set_fresh_node(fresh_node);
let incoming = task.incoming_tx.clone();
spawn_os_thread("engine", || {
increase_thread_priority();
@@ -739,11 +749,18 @@ where
// This validation **MUST** be instantly run in all cases even during active sync process.
let num_hash = payload.num_hash();
// Forward to snap sync orchestrator if active
self.forward_new_block_to_snap(&payload);
let engine_event = ConsensusEngineEvent::BlockReceived(num_hash);
self.emit_event(EngineApiEvent::BeaconConsensus(engine_event));
let block_hash = num_hash.hash;
// Extract BAL before the payload is consumed
let bal = payload.block_access_list().cloned();
// Check for invalid ancestors
if let Some(invalid) = self.find_invalid_ancestor(&payload) {
let status = self.handle_invalid_ancestor_payload(payload, invalid)?;
@@ -759,6 +776,13 @@ where
TreeOutcome::new(self.try_buffer_payload(payload)?)
};
// Cache BAL in the provider's store if the payload was accepted
if outcome.outcome.is_valid() {
if let Some(bal) = bal {
let _ = self.provider.bal_store().insert(block_hash, num_hash.number, bal);
}
}
// 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
@@ -1126,6 +1150,9 @@ where
// Record metrics
self.record_forkchoice_metrics();
// Forward head to snap sync orchestrator if active
self.forward_head_to_snap(state.head_block_hash);
// Pre-validation of forkchoice state
if let Some(early_result) = self.validate_forkchoice_state(state)? {
return Ok(TreeOutcome::new(early_result));
@@ -1321,6 +1348,17 @@ where
&self,
state: ForkchoiceState,
) -> ProviderResult<TreeOutcome<OnForkChoiceUpdated>> {
// For fresh nodes, trigger snap sync instead of downloading missing blocks
if self.snap.is_fresh_node() && self.backfill_sync_state.is_idle() {
debug!(target: "engine::tree", "Fresh node detected, triggering snap sync");
return Ok(TreeOutcome::new(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
PayloadStatusEnum::Syncing,
)))
.with_event(TreeEvent::BackfillAction(BackfillAction::StartSnapSync(
state.head_block_hash,
))));
}
// We don't have the block to perform the forkchoice update
// We assume the FCU is valid and at least the head is missing,
// so we need to start syncing to it
@@ -1511,9 +1549,9 @@ where
// Re-prepare overlay for the current canonical head with the new anchor.
// Spawn a background task to trigger computation so it's ready when the next payload
// arrives.
if let Some(overlay) = self.state.tree_state.prepare_canonical_overlay() {
if let Some(prepared) = self.state.tree_state.prepare_canonical_overlay() {
self.runtime.spawn_blocking_named("prepare-overlay", move || {
let _ = overlay.get();
let _ = prepared.overlay.get(prepared.anchor_hash);
});
}
@@ -1545,6 +1583,19 @@ where
}
return Ok(ops::ControlFlow::Break(()))
}
FromOrchestrator::SnapSyncStarted(events_tx) => {
debug!(target: "engine::tree", "received snap sync started event");
self.backfill_sync_state = BackfillSyncState::Active;
self.snap.start(events_tx);
// Replay latest known head if we can resolve it
if let Some(state) = self.state.forkchoice_state_tracker.sync_target_state() {
self.forward_head_to_snap(state.head_block_hash);
}
}
FromOrchestrator::SnapSyncFinished(outcome) => {
self.on_snap_sync_finished(outcome)?;
}
},
FromEngine::Request(request) => {
match request {
@@ -2790,7 +2841,10 @@ where
return Ok(None)
}
if !self.backfill_sync_state.is_idle() {
if self.snap.is_active() {
// During snap sync, forward downloaded blocks to the orchestrator
// for header persistence and BAL resolution
self.forward_downloaded_block_to_snap(&block);
return Ok(None)
}

View File

@@ -970,7 +970,7 @@ mod tests {
use rand::Rng;
use reth_chainspec::ChainSpec;
use reth_db_common::init::init_genesis;
use reth_ethereum_primitives::TransactionSigned;
use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
use reth_evm::OnStateHook;
use reth_evm_ethereum::EthEvmConfig;
use reth_primitives_traits::{Account, Recovered, StorageEntry};
@@ -1249,7 +1249,7 @@ mod tests {
StateProviderBuilder::new(provider_factory.clone(), genesis_hash, None),
OverlayStateProviderFactory::new(
provider_factory,
OverlayBuilder::new(ChangesetCache::new()),
OverlayBuilder::<EthPrimitives>::new(genesis_hash, ChangesetCache::new()),
),
&TreeConfig::default(),
);

View File

@@ -361,8 +361,44 @@ where
let (prefetch_tx, prefetch_rx) = oneshot::channel();
let (stream_tx, stream_rx) = oneshot::channel();
if let Some(to_sparse_trie_task) = to_sparse_trie_task {
let stream_ctx = ctx.clone();
executor.bal_streaming_pool().spawn(move || {
let branch_span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: &stream_parent_span,
"bal_hashed_state_stream",
bal_accounts = stream_bal.as_bal().len(),
);
let provider_parent_span = branch_span.clone();
let _span = branch_span.entered();
stream_bal.as_bal().par_iter().for_each_init(
|| {
(
stream_ctx.clone(),
None::<Box<dyn AccountReader>>,
provider_parent_span.clone(),
)
},
|(ctx, provider, parent_span), account_changes| {
ctx.send_bal_hashed_state(
parent_span,
provider,
account_changes,
&to_sparse_trie_task,
);
},
);
let _ = to_sparse_trie_task.send(StateRootMessage::FinishedStateUpdates);
let _ = stream_tx.send(());
});
} else {
let _ = stream_tx.send(());
}
if ctx.saved_cache.is_some() {
let prefetch_ctx = ctx.clone();
executor.prewarming_pool().spawn(move || {
let branch_span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
@@ -376,7 +412,7 @@ where
prefetch_bal.as_bal().par_iter().for_each_init(
|| {
(
prefetch_ctx.clone(),
ctx.clone(),
None::<CachedStateProvider<reth_provider::StateProviderBox, true>>,
provider_parent_span.clone(),
)
@@ -395,36 +431,6 @@ where
let _ = prefetch_tx.send(());
}
if let Some(to_sparse_trie_task) = to_sparse_trie_task {
executor.bal_streaming_pool().spawn(move || {
let branch_span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: &stream_parent_span,
"bal_hashed_state_stream",
bal_accounts = stream_bal.as_bal().len(),
);
let provider_parent_span = branch_span.clone();
let _span = branch_span.entered();
stream_bal.as_bal().par_iter().for_each_init(
|| (ctx.clone(), None::<Box<dyn AccountReader>>, provider_parent_span.clone()),
|(ctx, provider, parent_span), account_changes| {
ctx.send_bal_hashed_state(
parent_span,
provider,
account_changes,
&to_sparse_trie_task,
);
},
);
let _ = to_sparse_trie_task.send(StateRootMessage::FinishedStateUpdates);
let _ = stream_tx.send(());
});
} else {
let _ = stream_tx.send(());
}
prefetch_rx
.blocking_recv()
.expect("BAL prefetch task dropped without signaling completion");
@@ -751,7 +757,9 @@ where
provider: &mut Option<CachedStateProvider<reth_provider::StateProviderBox, true>>,
account: &alloy_eip7928::AccountChanges,
) {
if account.storage_changes.is_empty() && account.storage_reads.is_empty() {
if self.disable_bal_batch_io ||
(account.storage_changes.is_empty() && account.storage_reads.is_empty())
{
return;
}

View File

@@ -896,6 +896,7 @@ mod tests {
use reth_provider::{
providers::{OverlayBuilder, OverlayStateProviderFactory},
test_utils::create_test_provider_factory,
ChainSpecProvider,
};
use reth_trie_db::ChangesetCache;
use reth_trie_parallel::proof_task::ProofTaskCtx;
@@ -984,9 +985,13 @@ mod tests {
fn run_returns_parent_root_without_revealing_blind_trie_when_no_state_updates() {
let runtime = reth_tasks::Runtime::test();
let provider_factory = create_test_provider_factory();
let anchor_hash = provider_factory.chain_spec().genesis_hash();
let overlay_factory = OverlayStateProviderFactory::new(
provider_factory,
OverlayBuilder::new(ChangesetCache::new()),
OverlayBuilder::<reth_chain_state::EthPrimitives>::new(
anchor_hash,
ChangesetCache::new(),
),
);
let proof_worker_handle =
ProofWorkerHandle::new(&runtime, ProofTaskCtx::new(overlay_factory), false);

View File

@@ -62,7 +62,9 @@ use crate::tree::payload_processor::receipt_root_task::{IndexedReceipt, ReceiptR
use reth_chain_state::{
CanonicalInMemoryState, DeferredTrieData, ExecutedBlock, ExecutionTimingStats, LazyOverlay,
};
use reth_consensus::{ConsensusError, FullConsensus, ReceiptRootBloom};
use reth_consensus::{
validate_block_access_list_gas, ConsensusError, FullConsensus, ReceiptRootBloom,
};
use reth_engine_primitives::{
ConfigureEngineEvm, ExecutableTxIterator, ExecutionPayload, InvalidBlockHook, PayloadValidator,
};
@@ -527,8 +529,7 @@ where
// Create overlay factory for payload processor (StateRootTask path needs it for
// multiproofs)
let provider_factory = self.provider.clone();
let overlay_builder = OverlayBuilder::new(self.changeset_cache.clone())
.with_block_hash(Some(anchor_hash))
let overlay_builder = OverlayBuilder::<N>::new(anchor_hash, self.changeset_cache.clone())
.with_lazy_overlay(lazy_overlay);
let overlay_factory =
OverlayStateProviderFactory::new(provider_factory.clone(), overlay_builder.clone());
@@ -569,7 +570,7 @@ where
// The receipt root task is spawned before execution and receives receipts incrementally
// as transactions complete, allowing parallel computation during execution.
let execute_block_start = Instant::now();
let (output, senders, receipt_root_rx) =
let (output, senders, receipt_root_rx, built_bal) =
match self.execute_block(state_provider, env, &input, &mut handle) {
Ok(output) => output,
Err(err) => return self.handle_execution_error(input, err, &parent_block),
@@ -651,6 +652,7 @@ where
transaction_root,
receipt_root_bloom,
hashed_state,
built_bal
),
block
);
@@ -907,6 +909,7 @@ where
BlockExecutionOutput<N::Receipt>,
Vec<Address>,
tokio::sync::oneshot::Receiver<(B256, alloy_primitives::Bloom)>,
Option<BlockAccessList>,
),
InsertBlockErrorKind,
>
@@ -914,15 +917,29 @@ where
S: StateProvider + Send,
Err: core::error::Error + Send + Sync + 'static,
V: PayloadValidator<T, Block = N::Block>,
T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
T: PayloadTypes<
BuiltPayload: BuiltPayload<Primitives = N>,
ExecutionData: ExecutionPayload,
>,
Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
{
debug!(target: "engine::tree::payload_validator", "Executing block");
if let Some(bal_opt) = input.block_access_list() {
let bal = bal_opt.map_err(BlockExecutionError::other)?;
validate_block_access_list_gas(Some(&bal), input.gas_limit())
.map_err(|e| {
debug!(target: "engine::tree::payload_validator", "BAL is invalid since it contains more items than the gas limit allows");
InsertBlockErrorKind::Consensus(e)
})?
}
let has_bal = input.block_access_list().is_some();
let mut db = debug_span!(target: "engine::tree", "build_state_db").in_scope(|| {
State::builder()
.with_database(StateProviderDatabase::new(state_provider))
.with_bundle_update()
.with_bal_builder_if(has_bal)
.build()
});
@@ -981,6 +998,7 @@ where
handle.iter_transactions(),
&receipt_tx,
&executed_tx_index,
has_bal,
)?;
drop(receipt_tx);
@@ -995,6 +1013,11 @@ where
debug_span!(target: "engine::tree", "merge_transitions")
.in_scope(|| db.merge_transitions(BundleRetention::Reverts));
// Extract the built bal if payload has bal
let built_bal = if has_bal { db.take_built_alloy_bal() } else { None };
tracing::info!("Built Bal is {:?}", built_bal);
let output = BlockExecutionOutput { result, state: db.take_bundle() };
let execution_duration = execution_start.elapsed();
@@ -1002,7 +1025,7 @@ where
self.metrics.record_block_execution_gas_bucket(output.result.gas_used, execution_duration);
debug!(target: "engine::tree::payload_validator", elapsed = ?execution_duration, "Executed block");
Ok((output, senders, result_rx))
Ok((output, senders, result_rx, built_bal))
}
/// Executes transactions and collects senders, streaming receipts to a background task.
@@ -1014,18 +1037,20 @@ where
/// - Collecting transaction senders for later use
///
/// Returns the executor (for finalization) and the collected senders.
fn execute_transactions<E, Tx, InnerTx, Err>(
fn execute_transactions<'a, E, Tx, InnerTx, Err, DB>(
&self,
mut executor: E,
transaction_count: usize,
transactions: impl Iterator<Item = Result<Tx, Err>>,
receipt_tx: &crossbeam_channel::Sender<IndexedReceipt<N::Receipt>>,
executed_tx_index: &AtomicUsize,
has_bal: bool,
) -> Result<(E, Vec<Address>), BlockExecutionError>
where
E: BlockExecutor<Receipt = N::Receipt>,
E: BlockExecutor<Receipt = N::Receipt, Evm: alloy_evm::Evm<DB = &'a mut State<DB>>>,
Tx: alloy_evm::block::ExecutableTx<E> + alloy_evm::RecoveredTx<InnerTx>,
InnerTx: TxHashRef,
DB: revm::Database + 'a,
Err: core::error::Error + Send + Sync + 'static,
{
let mut senders = Vec::with_capacity(transaction_count);
@@ -1036,6 +1061,11 @@ where
.in_scope(|| executor.apply_pre_execution_changes())?;
self.metrics.record_pre_execution(pre_exec_start.elapsed());
// Bump BAL index after pre-execution changes (EIP-7928: index 0 is pre-execution)
if has_bal {
executor.evm_mut().db_mut().bump_bal_index();
}
// Execute transactions
let exec_span = debug_span!(target: "engine::tree", "execution").entered();
let mut transactions = transactions.into_iter();
@@ -1080,6 +1110,10 @@ where
let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
}
}
// Bump BAL index after each transaction (EIP-7928)
if has_bal {
executor.evm_mut().db_mut().bump_bal_index();
}
}
drop(exec_span);
@@ -1100,7 +1134,7 @@ where
fn compute_state_root_parallel(
&self,
provider_factory: P,
overlay_builder: OverlayBuilder,
overlay_builder: OverlayBuilder<N>,
hashed_state: &LazyHashedPostState,
) -> Result<(B256, TrieUpdates), ParallelStateRootError> {
let hashed_state = hashed_state.get();
@@ -1245,7 +1279,7 @@ where
&self,
state_provider_builder: StateProviderBuilder<N, P>,
provider_factory: P,
overlay_builder: OverlayBuilder,
overlay_builder: OverlayBuilder<N>,
hashed_state: &LazyHashedPostState,
task_trie_updates: TrieUpdates,
) -> bool {
@@ -1363,6 +1397,7 @@ where
transaction_root: Option<B256>,
receipt_root_bloom: Option<ReceiptRootBloom>,
hashed_state: LazyHashedPostState,
built_bal: Option<BlockAccessList>,
) -> Result<LazyHashedPostState, InsertBlockErrorKind>
where
V: PayloadValidator<T, Block = N::Block>,
@@ -1389,9 +1424,13 @@ where
let _enter =
debug_span!(target: "engine::tree::payload_validator", "validate_block_post_execution")
.entered();
if let Err(err) =
self.consensus.validate_block_post_execution(block, output, receipt_root_bloom)
{
if let Err(err) = self.consensus.validate_block_post_execution(
block,
output,
receipt_root_bloom,
built_bal,
) {
// call post-block hook
self.on_invalid_block(parent_block, block, output, None, ctx.state_mut());
return Err(err.into())
@@ -1448,7 +1487,7 @@ where
env: ExecutionEnv<Evm>,
txs: T,
provider_builder: StateProviderBuilder<N, P>,
overlay_factory: OverlayStateProviderFactory<P>,
overlay_factory: OverlayStateProviderFactory<P, N>,
strategy: StateRootStrategy,
) -> Result<
PayloadHandle<
@@ -1568,7 +1607,7 @@ where
fn get_parent_lazy_overlay(
parent_hash: B256,
state: &EngineApiTreeState<N>,
) -> (Option<LazyOverlay>, B256) {
) -> (Option<LazyOverlay<N>>, B256) {
// Get blocks leading to the parent to determine the anchor
let (anchor_hash, blocks) =
state.tree_state.blocks_by_hash(parent_hash).unwrap_or_else(|| (parent_hash, vec![]));
@@ -1596,10 +1635,7 @@ where
"Creating lazy overlay for in-memory blocks"
);
// Extract deferred trie data handles (non-blocking)
let handles: Vec<DeferredTrieData> = blocks.iter().map(|b| b.trie_data_handle()).collect();
(Some(LazyOverlay::new(anchor_hash, handles)), anchor_hash)
(Some(LazyOverlay::new(blocks)), anchor_hash)
}
/// Spawns a background task to compute and sort trie data for the executed block.
@@ -2033,8 +2069,7 @@ where
let (lazy_overlay, anchor_hash) = Self::get_parent_lazy_overlay(parent_hash, state);
let overlay_factory = OverlayStateProviderFactory::new(
self.provider.clone(),
OverlayBuilder::new(self.changeset_cache.clone())
.with_block_hash(Some(anchor_hash))
OverlayBuilder::<N>::new(anchor_hash, self.changeset_cache.clone())
.with_lazy_overlay(lazy_overlay),
);

View File

@@ -266,6 +266,7 @@ mod tests {
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: Bytes::default(),
})
})
@@ -280,6 +281,7 @@ mod tests {
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
};
@@ -314,6 +316,7 @@ mod tests {
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
})
}
@@ -331,6 +334,7 @@ mod tests {
state_gas_used: 0,
reservoir: 0,
gas_refunded: 0,
refill_amount: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
})
}

View File

@@ -0,0 +1,174 @@
//! Snap sync helpers for [`EngineApiTreeHandler`].
use crate::backfill::{BackfillAction, BackfillSyncState};
use alloy_consensus::BlockHeader;
use alloy_eips::BlockNumHash;
use alloy_primitives::B256;
use reth_engine_primitives::ExecutionPayload;
use reth_evm::ConfigureEvm;
use reth_payload_primitives::{BuiltPayload, PayloadTypes};
use reth_primitives_traits::{NodePrimitives, SealedBlock};
use reth_provider::{
BalProvider, BlockReader, ChangeSetReader, DatabaseProviderFactory, HashedPostStateProvider,
StageCheckpointReader, StateProviderFactory, StateReader, StorageChangeSetReader,
StorageSettingsCache,
};
use tokio::sync::mpsc::UnboundedSender;
use tracing::*;
use super::{
error::InsertBlockFatalError, payload_validator::EngineValidator, EngineApiEvent,
EngineApiTreeHandler, WaitForCaches,
};
/// Snap sync state owned by the engine tree.
#[derive(Debug, Default)]
pub(super) struct SnapTreeState {
events_tx: Option<UnboundedSender<reth_engine_snap::SnapSyncEvent>>,
fresh_node: bool,
}
impl SnapTreeState {
/// Creates snap tree state for the given node freshness.
pub(super) const fn new(fresh_node: bool) -> Self {
Self { events_tx: None, fresh_node }
}
/// Returns true if this node started with no persisted blocks.
pub(super) const fn is_fresh_node(&self) -> bool {
self.fresh_node
}
/// Updates the fresh-node flag.
pub(super) const fn set_fresh_node(&mut self, fresh_node: bool) {
self.fresh_node = fresh_node;
}
/// Returns true if snap sync is currently receiving engine events.
pub(super) const fn is_active(&self) -> bool {
self.events_tx.is_some()
}
/// Starts forwarding engine events to snap sync.
pub(super) fn start(&mut self, events_tx: UnboundedSender<reth_engine_snap::SnapSyncEvent>) {
self.events_tx = Some(events_tx);
}
/// Marks snap sync as finished.
pub(super) fn finish(&mut self) {
self.events_tx = None;
self.fresh_node = false;
}
fn events_tx(&self) -> Option<&UnboundedSender<reth_engine_snap::SnapSyncEvent>> {
self.events_tx.as_ref()
}
}
impl<N, P, T, V, C> EngineApiTreeHandler<N, P, T, V, C>
where
N: NodePrimitives,
P: DatabaseProviderFactory
+ BlockReader<Block = N::Block, Header = N::BlockHeader>
+ StateProviderFactory
+ StateReader<Receipt = N::Receipt>
+ HashedPostStateProvider
+ BalProvider
+ Clone
+ 'static,
P::Provider: BlockReader<Block = N::Block, Header = N::BlockHeader>
+ StageCheckpointReader
+ ChangeSetReader
+ StorageChangeSetReader
+ StorageSettingsCache,
C: ConfigureEvm<Primitives = N> + 'static,
T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
V: EngineValidator<T> + WaitForCaches,
{
/// Forwards a new block event to the snap sync orchestrator, if active.
pub(super) fn forward_new_block_to_snap(&self, payload: &T::ExecutionData) {
if let Some(events_tx) = self.snap.events_tx() {
let _ = events_tx.send(reth_engine_snap::SnapSyncEvent::NewBlock {
number: payload.block_number(),
hash: payload.block_hash(),
state_root: payload.state_root(),
parent_hash: payload.parent_hash(),
bal: payload.block_access_list().cloned(),
});
}
}
/// Forwards a new head event to the snap sync orchestrator, if active.
pub(super) fn forward_head_to_snap(&self, head_hash: B256) {
if let Some(events_tx) = self.snap.events_tx() {
let _ = events_tx.send(reth_engine_snap::SnapSyncEvent::NewHead { head_hash });
}
}
/// Forwards a downloaded block event to the snap sync orchestrator, if active.
pub(super) fn forward_downloaded_block_to_snap(&self, block: &SealedBlock<N::Block>) {
if let Some(events_tx) = self.snap.events_tx() {
let _ = events_tx.send(reth_engine_snap::SnapSyncEvent::DownloadedBlock {
number: block.number(),
hash: block.hash(),
state_root: block.header().state_root(),
parent_hash: block.header().parent_hash(),
});
}
}
/// Handles snap sync completion.
pub(super) fn on_snap_sync_finished(
&mut self,
outcome: reth_engine_snap::SnapSyncOutcome,
) -> Result<(), InsertBlockFatalError> {
debug!(target: "engine::tree", synced_to = outcome.synced_to, %outcome.block_hash, "snap sync finished");
self.backfill_sync_state = BackfillSyncState::Idle;
self.snap.finish();
let backfill_height = outcome.synced_to;
let backfill_hash = outcome.block_hash;
// Remove all blocks below the snap sync height
self.state.buffer.remove_old_blocks(backfill_height);
self.purge_timing_stats(backfill_height, None);
self.canonical_in_memory_state.clear_state();
// Update canonical head — try DB first, fall back to outcome data
if let Ok(Some(new_head)) = self.provider.sealed_header(backfill_height) {
self.state.tree_state.set_canonical_head(new_head.num_hash());
self.persistence_state.finish(new_head.hash(), new_head.number());
self.canonical_in_memory_state.set_canonical_head(new_head);
} else {
let num_hash = BlockNumHash { hash: backfill_hash, number: backfill_height };
self.state.tree_state.set_canonical_head(num_hash);
self.persistence_state.finish(backfill_hash, backfill_height);
}
// Remove executed blocks below the snap sync height
let backfill_num_hash = self
.provider
.block_hash(backfill_height)?
.map(|hash| BlockNumHash { hash, number: backfill_height })
.unwrap_or(BlockNumHash { hash: backfill_hash, number: backfill_height });
self.state.tree_state.remove_until(
backfill_num_hash,
self.persistence_state.last_persisted_block.hash,
Some(backfill_num_hash),
);
self.metrics.engine.executed_blocks.set(self.state.tree_state.block_count() as f64);
self.metrics.tree.canonical_chain_height.set(backfill_height as f64);
// Trigger a pipeline run for MerkleExecute + Finish.
// The orchestrator already set all other stage checkpoints to the snap target,
// so only MerkleExecute (builds AccountsTrie/StoragesTrie from hashed leaves)
// and Finish will run.
self.emit_event(EngineApiEvent::BackfillAction(BackfillAction::Start(
backfill_hash.into(),
)));
self.backfill_sync_state = BackfillSyncState::Pending;
Ok(())
}
}

View File

@@ -6,7 +6,7 @@ use alloy_primitives::{
map::{B256Map, B256Set},
BlockNumber, B256,
};
use reth_chain_state::{DeferredTrieData, EthPrimitives, ExecutedBlock, LazyOverlay};
use reth_chain_state::{EthPrimitives, ExecutedBlock, LazyOverlay};
use reth_primitives_traits::{AlloyBlockHeader, NodePrimitives, SealedHeader};
use std::{
collections::{btree_map, hash_map, BTreeMap, VecDeque},
@@ -43,7 +43,7 @@ pub struct TreeState<N: NodePrimitives = EthPrimitives> {
/// This is optimistically prepared after the canonical head changes, so that
/// the next payload building on the canonical head can use it immediately
/// without recomputing.
pub(crate) cached_canonical_overlay: Option<PreparedCanonicalOverlay>,
pub(crate) cached_canonical_overlay: Option<PreparedCanonicalOverlay<N>>,
}
impl<N: NodePrimitives> TreeState<N> {
@@ -106,10 +106,10 @@ impl<N: NodePrimitives> TreeState<N> {
/// This should be called after the canonical head changes to optimistically
/// prepare the overlay for the next payload that will likely build on it.
///
/// Returns a clone of the [`LazyOverlay`] so the caller can spawn a background
/// task to trigger computation via [`LazyOverlay::get`]. This ensures the overlay
/// is actually computed before the next payload arrives.
pub(crate) fn prepare_canonical_overlay(&mut self) -> Option<LazyOverlay> {
/// Returns a clone of the prepared overlay so the caller can spawn a background
/// task to trigger computation via [`LazyOverlay::get`] for the cached anchor.
/// This ensures the overlay is actually computed before the next payload arrives.
pub(crate) fn prepare_canonical_overlay(&mut self) -> Option<PreparedCanonicalOverlay<N>> {
let canonical_hash = self.current_canonical_head.hash;
// Get blocks leading to the canonical head
@@ -119,25 +119,23 @@ impl<N: NodePrimitives> TreeState<N> {
return None;
};
// Extract deferred trie data handles from blocks (newest to oldest)
let handles: Vec<DeferredTrieData> = blocks.iter().map(|b| b.trie_data_handle()).collect();
let overlay = LazyOverlay::new(anchor_hash, handles);
self.cached_canonical_overlay = Some(PreparedCanonicalOverlay {
let num_blocks = blocks.len();
let prepared = PreparedCanonicalOverlay {
parent_hash: canonical_hash,
overlay: overlay.clone(),
overlay: LazyOverlay::new(blocks),
anchor_hash,
});
};
self.cached_canonical_overlay = Some(prepared.clone());
debug!(
target: "engine::tree",
%canonical_hash,
%anchor_hash,
num_blocks = blocks.len(),
num_blocks,
"Prepared cached canonical overlay"
);
Some(overlay)
Some(prepared)
}
/// Returns the cached overlay if it matches the requested parent hash and anchor.
@@ -148,7 +146,7 @@ impl<N: NodePrimitives> TreeState<N> {
&self,
parent_hash: B256,
expected_anchor: B256,
) -> Option<&PreparedCanonicalOverlay> {
) -> Option<&PreparedCanonicalOverlay<N>> {
self.cached_canonical_overlay.as_ref().filter(|cached| {
cached.parent_hash == parent_hash && cached.anchor_hash == expected_anchor
})
@@ -429,10 +427,10 @@ impl<N: NodePrimitives> TreeState<N> {
/// the next payload (which typically builds on the canonical head) to reuse
/// the pre-computed overlay immediately without re-traversing in-memory blocks.
///
/// The overlay captures deferred trie data handles from all in-memory blocks
/// The overlay captures executed blocks from all in-memory blocks
/// between the canonical head and the persisted anchor. When a new payload
/// arrives building on the canonical head, this cached overlay can be used
/// directly instead of calling `blocks_by_hash` and collecting handles again.
/// directly instead of calling `blocks_by_hash` again.
///
/// # Invalidation
///
@@ -440,16 +438,16 @@ impl<N: NodePrimitives> TreeState<N> {
/// - Persistence completes (anchor changes)
/// - The canonical head changes to a different block
#[derive(Debug, Clone)]
pub struct PreparedCanonicalOverlay {
pub struct PreparedCanonicalOverlay<N: NodePrimitives = EthPrimitives> {
/// The block hash for which this overlay is prepared as a parent.
///
/// When a payload arrives with this parent hash, the overlay can be reused.
pub parent_hash: B256,
/// The pre-computed lazy overlay containing deferred trie data handles.
/// The pre-computed lazy overlay containing executed blocks for the canonical segment.
///
/// This is computed optimistically after `set_canonical_head` so subsequent
/// payloads don't need to re-collect the handles.
pub overlay: LazyOverlay,
/// This is computed optimistically after `set_canonical_head` so subsequent payloads don't
/// need to walk the in-memory chain again.
pub overlay: LazyOverlay<N>,
/// The anchor hash (persisted ancestor) this overlay is based on.
///
/// Used to verify the overlay is still valid (anchor hasn't changed due to persistence).

View File

@@ -287,7 +287,7 @@ where
let tx_recovered =
tx.try_into_recovered().map_err(|_| ProviderError::SenderRecoveryError)?;
let gas_used = match builder.execute_transaction(tx_recovered) {
Ok(gas_used) => gas_used,
Ok(gas_used) => gas_used.tx_gas_used(),
Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
hash,
error,

View File

@@ -13,7 +13,7 @@ extern crate alloc;
use alloc::{fmt::Debug, sync::Arc};
use alloy_consensus::{constants::MAXIMUM_EXTRA_DATA_SIZE, EMPTY_OMMER_ROOT_HASH};
use alloy_eips::eip7840::BlobParams;
use alloy_eips::{eip7840::BlobParams, eip7928::BlockAccessList};
use reth_chainspec::{EthChainSpec, EthereumHardforks};
use reth_consensus::{
Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom, TransactionRoot,
@@ -108,9 +108,15 @@ where
block: &RecoveredBlock<N::Block>,
result: &BlockExecutionResult<N::Receipt>,
receipt_root_bloom: Option<ReceiptRootBloom>,
block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError> {
let res =
validate_block_post_execution(block, &self.chain_spec, result, receipt_root_bloom);
let res = validate_block_post_execution(
block,
&self.chain_spec,
result,
receipt_root_bloom,
block_access_list,
);
if self.skip_requests_hash_check &&
let Err(ConsensusError::BodyRequestsHashDiff(_)) = &res

View File

@@ -1,6 +1,9 @@
use alloc::vec::Vec;
use alloy_consensus::{proofs::calculate_receipt_root, BlockHeader, TxReceipt};
use alloy_eips::Encodable2718;
use alloy_eips::{
eip7928::{compute_block_access_list_hash, BlockAccessList},
Encodable2718,
};
use alloy_primitives::{Bloom, Bytes, B256};
use reth_chainspec::EthereumHardforks;
use reth_consensus::ConsensusError;
@@ -21,6 +24,7 @@ pub fn validate_block_post_execution<B, R, ChainSpec>(
chain_spec: &ChainSpec,
result: &BlockExecutionResult<R>,
receipt_root_bloom: Option<(B256, Bloom)>,
block_access_list: Option<BlockAccessList>,
) -> Result<(), ConsensusError>
where
B: Block,
@@ -79,6 +83,21 @@ where
}
}
// Validate that the block access list hash matches the calculated block access list hash
if chain_spec.is_amsterdam_active_at_timestamp(block.header().timestamp()) &&
block_access_list.is_some()
{
let block_bal_hash = block.header().block_access_list_hash().unwrap_or_default();
let default_bal = BlockAccessList::default();
let block_access_list_hash =
compute_block_access_list_hash(block_access_list.as_ref().unwrap_or(&default_bal));
if block_access_list_hash != block_bal_hash {
return Err(ConsensusError::BlockAccessListHashMismatch(
(block_access_list_hash, block_bal_hash).into(),
))
}
}
Ok(())
}

View File

@@ -54,6 +54,10 @@ impl<
) -> Self::ExecutionData {
T::block_to_payload(block)
}
fn built_payload_to_execution_data(payload: &Self::BuiltPayload) -> Self::ExecutionData {
T::built_payload_to_execution_data(payload)
}
}
impl<T> EngineTypes for EthEngineTypes<T>
@@ -94,4 +98,20 @@ impl PayloadTypes for EthPayloadTypes {
ExecutionPayload::from_block_unchecked(block.hash(), &block.into_block());
ExecutionData { payload, sidecar }
}
fn built_payload_to_execution_data(payload: &Self::BuiltPayload) -> Self::ExecutionData {
if let Some(bal) = payload.block_access_list() {
let block = payload.block();
let raw_block = block.clone().into_block();
let sidecar = alloy_rpc_types_engine::ExecutionPayloadSidecar::from_block(&raw_block);
let v4 = alloy_rpc_types_engine::ExecutionPayloadV4::from_block_unchecked_with_bal(
block.hash(),
&raw_block,
bal.clone(),
);
ExecutionData { payload: ExecutionPayload::V4(v4), sidecar }
} else {
Self::block_to_payload(payload.block().clone())
}
}
}

View File

@@ -205,6 +205,10 @@ impl<N: NodePrimitives> BuiltPayload for EthBuiltPayload<N> {
fn requests(&self) -> Option<Requests> {
self.requests.clone()
}
fn block_access_list(&self) -> Option<&Bytes> {
self.block_access_list.as_ref()
}
}
// V1 engine_getPayloadV1 response

View File

@@ -47,6 +47,7 @@ where
transactions,
output: BlockExecutionResult { receipts, requests, gas_used, blob_gas_used },
state_root,
block_access_list_hash,
..
} = input;
@@ -90,6 +91,12 @@ where
};
}
let bal_hash = if self.chain_spec.is_amsterdam_active_at_timestamp(timestamp) {
block_access_list_hash
} else {
None
};
let header = Header {
parent_hash: ctx.parent_hash,
ommers_hash: EMPTY_OMMER_ROOT_HASH,
@@ -112,8 +119,8 @@ where
blob_gas_used: block_blob_gas_used,
excess_blob_gas,
requests_hash,
block_access_list_hash: None,
slot_number: None,
block_access_list_hash: bal_hash,
slot_number: ctx.slot_number,
};
Ok(Block {

View File

@@ -54,13 +54,19 @@ revm = { workspace = true, features = ["secp256k1", "blst", "c-kzg", "memory_lim
eyre.workspace = true
[dev-dependencies]
reth-config.workspace = true
reth-db.workspace = true
reth-ethereum-consensus.workspace = true
reth-exex.workspace = true
reth-engine-snap = { workspace = true, features = ["test-utils"] }
reth-node-core.workspace = true
reth-e2e-test-utils.workspace = true
reth-prune-types.workspace = true
reth-stages = { workspace = true, features = ["test-utils"] }
reth-stages-types.workspace = true
reth-static-file.workspace = true
reth-tasks.workspace = true
reth-testing-utils.workspace = true
reth-stages-types.workspace = true
tempfile.workspace = true
jsonrpsee-core.workspace = true
@@ -112,4 +118,7 @@ test-utils = [
"reth-evm-ethereum/test-utils",
"reth-stages-types/test-utils",
"reth-tasks/test-utils",
"reth-prune-types/test-utils",
"reth-stages/test-utils",
"reth-engine-snap/test-utils",
]

View File

@@ -2,6 +2,7 @@ use crate::utils::{advance_with_random_transactions, eth_payload_attributes};
use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope};
use alloy_eips::Encodable2718;
use alloy_network::TxSignerSync;
use alloy_primitives::B256;
use alloy_provider::{Provider, ProviderBuilder};
use futures::future::JoinAll;
use rand::{rngs::StdRng, seq::IndexedRandom, Rng, SeedableRng};
@@ -294,3 +295,194 @@ async fn test_tx_propagation() -> eyre::Result<()> {
Ok(())
}
#[tokio::test]
#[ignore = "requires serving in-memory state; serving node keeps ~2 blocks unpersisted"]
async fn can_snap_sync_frozen_head() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let seed: [u8; 32] = rand::rng().random();
let mut rng = StdRng::from_seed(seed);
println!("Seed: {seed:?}");
let chain_spec = Arc::new(
ChainSpecBuilder::default()
.chain(MAINNET.chain)
.genesis(serde_json::from_str(include_str!("../assets/genesis.json")).unwrap())
.cancun_activated()
.prague_activated()
.build(),
);
// Do NOT auto-connect nodes — we want to prevent accidental eth sync
let (mut nodes, _) = setup_engine_with_connection::<EthereumNode>(
2,
chain_spec,
false,
Default::default(),
eth_payload_attributes,
false, // do not auto-connect
)
.await?;
let mut node_b = nodes.pop().unwrap();
let mut node_a = nodes.pop().unwrap();
// Advance Node A by 300 blocks with random transactions (creates contracts + storage)
advance_with_random_transactions(&mut node_a, 300, &mut rng, true).await?;
// Wait for hashed state to stabilize in MDBX
let _target_root = {
let mut prev = node_a.snap_state_root().await;
loop {
tokio::time::sleep(Duration::from_millis(500)).await;
let current = node_a.snap_state_root().await;
if current == prev && current != B256::ZERO {
break current;
}
prev = current;
}
};
let target_hash = node_a.block_hash(300);
// Connect Node B to Node A (after blocks are produced, head is frozen)
node_b.connect(&mut node_a).await;
// Trigger engine-driven snap sync: send the head FCU so the engine tree
// detects a fresh node and starts the SnapSyncOrchestrator.
node_b.sync_to(target_hash).await?;
// Verify state root matches Node A
let node_b_root = node_b.snap_state_root().await;
let node_a_root = node_a.snap_state_root().await;
assert_eq!(node_b_root, node_a_root, "State roots should match after snap sync");
Ok(())
}
#[tokio::test]
async fn can_snap_sync_catch_up() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let seed: [u8; 32] = rand::rng().random();
let mut rng = StdRng::from_seed(seed);
println!("Seed: {seed:?}");
let chain_spec = Arc::new(
ChainSpecBuilder::default()
.chain(MAINNET.chain)
.genesis(serde_json::from_str(include_str!("../assets/genesis.json")).unwrap())
.cancun_activated()
.prague_activated()
.amsterdam_activated()
.build(),
);
// Do NOT auto-connect — prevent accidental eth sync
let (mut nodes, _) = setup_engine_with_connection::<EthereumNode>(
2,
chain_spec,
false,
Default::default(),
eth_payload_attributes,
false,
)
.await?;
let mut node_b = nodes.pop().unwrap();
let mut node_a = nodes.pop().unwrap();
// Build initial state on Node A (20 blocks)
advance_with_random_transactions(&mut node_a, 20, &mut rng, true).await?;
let initial_target = node_a.block_hash(20);
// Connect Node B to Node A
node_b.connect(&mut node_a).await;
// Advance Node A further BEFORE triggering snap sync on Node B.
advance_with_random_transactions(&mut node_a, 10, &mut rng, true).await?;
// Now trigger snap sync on Node B targeting the initial block.
node_b.update_forkchoice(initial_target, initial_target).await?;
// Continue advancing Node A to push even further
advance_with_random_transactions(&mut node_a, 5, &mut rng, true).await?;
let final_hash = node_a.block_hash(35);
// Wait for Node B to sync
node_b.sync_to(final_hash).await?;
Ok(())
}
/// Tests that the snap sync orchestrator recovers when the pivot root becomes
/// stale. The test lowers the serving lookback, then advances Node A far enough
/// that the snap server's lookback window no longer covers the original pivot
/// root, forcing the orchestrator to re-resolve the head and advance the pivot.
#[tokio::test]
async fn can_snap_sync_stale_pivot() -> eyre::Result<()> {
reth_tracing::init_test_tracing();
let _lookback_guard = reth_engine_snap::serve::set_max_serving_lookback_for_tests(24);
let seed: [u8; 32] = rand::rng().random();
let mut rng = StdRng::from_seed(seed);
println!("Seed: {seed:?}");
let chain_spec = Arc::new(
ChainSpecBuilder::default()
.chain(MAINNET.chain)
.genesis(serde_json::from_str(include_str!("../assets/genesis.json")).unwrap())
.cancun_activated()
.prague_activated()
.amsterdam_activated()
.build(),
);
let (mut nodes, wallet) = setup_engine_with_connection::<EthereumNode>(
2,
chain_spec,
false,
Default::default(),
eth_payload_attributes,
false,
)
.await?;
let mut node_b = nodes.pop().unwrap();
let mut node_a = nodes.pop().unwrap();
// Build enough stateful blocks for snap sync to exercise account/storage
// download, then move the head forward with one cheap transfer per block.
// With the test lookback set to 24, tip 30 starts serving at block 6, so the
// original pivot root (block 4 = 20 - PIVOT_OFFSET) is just stale.
advance_with_random_transactions(&mut node_a, 20, &mut rng, true).await?;
let old_target = node_a.block_hash(20);
let chain_id = wallet.chain_id;
let padding_wallet = Wallet::new(2).with_chain_id(chain_id).wallet_gen().pop().unwrap();
for nonce in 0..10 {
let raw_tx = TransactionTestContext::transfer_tx_bytes_with_nonce(
chain_id,
padding_wallet.clone(),
nonce,
)
.await;
node_a.rpc.inject_tx(raw_tx).await?;
node_a.advance_block().await?;
}
let final_hash = node_a.block_hash(30);
// Connect Node B to Node A and trigger snap sync targeting block 20.
// Orchestrator picks pivot = 20 - 16 = 4, whose root is stale.
node_b.connect(&mut node_a).await;
node_b.update_forkchoice(old_target, old_target).await?;
// Node B should recover from the stale pivot, re-resolve head, and sync.
node_b.sync_to(final_hash).await?;
Ok(())
}

View File

@@ -24,7 +24,7 @@ pub(crate) const fn eth_payload_attributes(timestamp: u64) -> PayloadAttributes
suggested_fee_recipient: Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
slot_number: None,
slot_number: Some(0),
}
}

View File

@@ -9,7 +9,7 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
use alloy_consensus::Transaction;
use alloy_primitives::U256;
use alloy_primitives::{Bytes, U256};
use alloy_rlp::Encodable;
use alloy_rpc_types_engine::PayloadAttributes as EthPayloadAttributes;
use reth_basic_payload_builder::{
@@ -21,6 +21,7 @@ use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
use reth_errors::{BlockExecutionError, BlockValidationError, ConsensusError};
use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
use reth_evm::{
block::TxResult,
execute::{BlockBuilder, BlockBuilderOutcome, BlockExecutor},
ConfigureEvm, Evm, NextBlockEnvAttributes,
};
@@ -37,7 +38,7 @@ use reth_transaction_pool::{
BestTransactions, BestTransactionsAttributes, PoolTransaction, TransactionPool,
ValidPoolTransaction,
};
use revm::context_interface::Block as _;
use revm::context_interface::{Block as _, Cfg as _};
use std::sync::Arc;
use tracing::{debug, trace, warn};
@@ -204,8 +205,11 @@ where
.map_err(PayloadBuilderError::other)?;
debug!(target: "payload_builder", id=%payload_id, parent_header = ?parent_header.hash(), parent_number = parent_header.number, "building new payload");
let mut cumulative_gas_used = 0;
let mut cumulative_tx_gas_used = 0;
let mut block_regular_gas_used = 0;
let mut block_state_gas_used = 0;
let block_gas_limit: u64 = builder.evm_mut().block().gas_limit();
let tx_gas_limit_cap = builder.evm_mut().cfg_env().tx_gas_limit_cap();
let base_fee = builder.evm_mut().block().basefee();
let mut best_txs = best_txs(BestTransactionsAttributes::new(
@@ -250,20 +254,41 @@ where
while let Some(pool_tx) = best_txs.next() {
// ensure we still have capacity for this transaction
if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
let exceeds_gas_limit = if is_amsterdam {
let regular_available_gas = block_gas_limit.saturating_sub(block_regular_gas_used);
let state_available_gas = block_gas_limit.saturating_sub(block_state_gas_used);
let regular_tx_gas_limit = pool_tx.gas_limit().min(tx_gas_limit_cap);
if regular_tx_gas_limit > regular_available_gas {
Some((regular_tx_gas_limit, regular_available_gas))
} else if pool_tx.gas_limit() > state_available_gas {
Some((pool_tx.gas_limit(), state_available_gas))
} else {
None
}
} else {
let block_available_gas = block_gas_limit.saturating_sub(cumulative_tx_gas_used);
(pool_tx.gas_limit() > block_available_gas)
.then_some((pool_tx.gas_limit(), block_available_gas))
};
if let Some((transaction_gas_limit, block_available_gas)) = exceeds_gas_limit {
// we can't fit this transaction into the block, so we need to mark it as invalid
// which also removes all dependent transaction from the iterator before we can
// continue
best_txs.mark_invalid(
&pool_tx,
&InvalidPoolTransactionError::ExceedsGasLimit(pool_tx.gas_limit(), block_gas_limit),
&InvalidPoolTransactionError::ExceedsGasLimit(
transaction_gas_limit,
block_available_gas,
),
);
continue
continue;
}
// check if the job was cancelled, if so we can exit early
if cancel.is_cancelled() {
return Ok(BuildOutcome::Cancelled)
return Ok(BuildOutcome::Cancelled);
}
// convert tx to a signed transaction
@@ -282,7 +307,7 @@ where
limit: MAX_RLP_BLOCK_SIZE,
},
);
continue
continue;
}
// There's only limited amount of blob space available per block, so we need to check if
@@ -306,14 +331,14 @@ where
},
),
);
continue
continue;
}
let blob_sidecar_result = 'sidecar: {
let Some(sidecar) =
pool.get_blob(*tx.hash()).map_err(PayloadBuilderError::other)?
else {
break 'sidecar Err(Eip4844PoolTransactionError::MissingEip4844BlobSidecar)
break 'sidecar Err(Eip4844PoolTransactionError::MissingEip4844BlobSidecar);
};
if is_osaka {
@@ -333,7 +358,7 @@ where
Ok(sidecar) => Some(sidecar),
Err(error) => {
best_txs.mark_invalid(&pool_tx, &InvalidPoolTransactionError::Eip4844(error));
continue
continue;
}
};
}
@@ -341,8 +366,11 @@ where
let miner_fee = tx.effective_tip_per_gas(base_fee);
let tx_hash = *tx.tx_hash();
let gas_used = match builder.execute_transaction(tx) {
Ok(gas_used) => gas_used,
let mut tx_regular_gas_used = 0;
let gas_output = match builder.execute_transaction_with_result_closure(tx, |result| {
tx_regular_gas_used = result.result().result.gas().block_regular_gas_used();
}) {
Ok(gas_output) => gas_output,
Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
error, ..
})) => {
@@ -360,11 +388,10 @@ where
),
);
}
continue
continue;
}
// EIP-7778: the executor tracks gas_before_refund while the payload builder's
// pre-check uses gas_after_refund. Near-full blocks can pass the pre-check but
// fail the executor's check. Skip the tx and continue building.
// The executor is the source of truth for block gas availability. Keep this
// non-fatal in case local builder accounting diverges from executor rules.
Err(BlockExecutionError::Validation(
BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
transaction_gas_limit,
@@ -379,7 +406,7 @@ where
block_available_gas,
),
);
continue
continue;
}
// this is an error that we should treat as fatal for this attempt
Err(err) => return Err(PayloadBuilderError::evm(err)),
@@ -398,9 +425,12 @@ where
block_transactions_rlp_length += tx_rlp_len;
// update and add to total fees
let gas_used = gas_output.tx_gas_used();
let miner_fee = miner_fee.expect("fee is always valid; execution succeeded");
total_fees += U256::from(miner_fee) * U256::from(gas_used);
cumulative_gas_used += gas_used;
cumulative_tx_gas_used += gas_used;
block_regular_gas_used += tx_regular_gas_used;
block_state_gas_used += gas_output.state_gas_used();
// Add blob tx sidecar to the payload.
if let Some(sidecar) = blob_tx_sidecar {
@@ -413,10 +443,12 @@ where
// Release db
drop(builder);
// can skip building the block
return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads });
}
let BlockBuilderOutcome { execution_result, block, .. } = if let Some(mut handle) = trie_handle
let BlockBuilderOutcome { execution_result, block, block_access_list, .. } = if let Some(
mut handle,
) = trie_handle
{
// Drop the state hook, which drops the StateHookSender and triggers
// FinishedStateUpdates via its Drop impl, signaling the trie task to finalize.
@@ -455,8 +487,10 @@ where
max_rlp_length: MAX_RLP_BLOCK_SIZE,
}));
}
let block_access_list: Option<Bytes> =
block_access_list.map(|block_access_list| alloy_rlp::encode(&block_access_list).into());
let payload = EthBuiltPayload::new(sealed_block, total_fees, requests, None)
let payload = EthBuiltPayload::new(sealed_block, total_fees, requests, block_access_list)
// add blob sidecars from the executed txs
.with_sidecars(blob_sidecars);

View File

@@ -79,4 +79,11 @@ where
Self::Right(b) => b.size_hint(),
}
}
fn take_bal(&mut self) -> Option<alloy_eips::eip7928::BlockAccessList> {
match self {
Self::Left(a) => a.take_bal(),
Self::Right(b) => b.take_bal(),
}
}
}

View File

@@ -3,8 +3,11 @@
use crate::{ConfigureEvm, Database, OnStateHook, TxEnvFor};
use alloc::{boxed::Box, sync::Arc, vec::Vec};
use alloy_consensus::{BlockHeader, Header};
use alloy_eips::eip2718::WithEncoded;
pub use alloy_evm::block::{BlockExecutor, BlockExecutorFactory};
use alloy_eips::{
eip2718::WithEncoded,
eip7928::{compute_block_access_list_hash, BlockAccessList},
};
pub use alloy_evm::block::{BlockExecutor, BlockExecutorFactory, GasOutput};
use alloy_evm::{
block::{CommitChanges, ExecutableTxParts},
Evm, EvmEnv, EvmFactory, RecoveredTx, ToTxEnv,
@@ -21,7 +24,10 @@ use reth_primitives_traits::{
use reth_storage_api::StateProvider;
pub use reth_storage_errors::provider::ProviderError;
use reth_trie_common::{updates::TrieUpdates, HashedPostState};
use revm::database::{states::bundle_state::BundleRetention, BundleState, State};
use revm::{
database::{states::bundle_state::BundleRetention, BundleState, State},
state::bal::Bal,
};
/// A type that knows how to execute a block. It is assumed to operate on a
/// [`crate::Evm`] internally and use [`State`] as database.
@@ -145,6 +151,9 @@ pub trait Executor<DB: Database>: Sized {
///
/// This is used to optimize DB commits depending on the size of the state.
fn size_hint(&self) -> usize;
/// Take built [`BlockAccessList`] from executor
fn take_bal(&mut self) -> Option<BlockAccessList>;
}
/// Input for block building. Consumed by [`BlockAssembler`].
@@ -162,6 +171,7 @@ pub trait Executor<DB: Database>: Sized {
/// - `bundle_state`: Accumulated state changes from all transactions
/// - `state_provider`: Access to the current state for additional lookups
/// - `state_root`: The calculated state root after all changes
/// - `block_access_list_hash`: Block access list hash (EIP-7928, Amsterdam)
///
/// # Usage
///
@@ -178,6 +188,7 @@ pub trait Executor<DB: Database>: Sized {
/// bundle_state: &state_changes,
/// state_provider: &state,
/// state_root: calculated_root,
/// block_access_list_hash: Some(calculated_bal_hash),
/// };
///
/// let block = assembler.assemble_block(input)?;
@@ -205,6 +216,8 @@ pub struct BlockAssemblerInput<'a, 'b, F: BlockExecutorFactory, H = Header> {
pub state_provider: &'b dyn StateProvider,
/// State root for this block.
pub state_root: B256,
/// Block access list hash (EIP-7928, Amsterdam).
pub block_access_list_hash: Option<B256>,
}
impl<'a, 'b, F: BlockExecutorFactory, H> BlockAssemblerInput<'a, 'b, F, H> {
@@ -222,6 +235,7 @@ impl<'a, 'b, F: BlockExecutorFactory, H> BlockAssemblerInput<'a, 'b, F, H> {
bundle_state: &'a BundleState,
state_provider: &'b dyn StateProvider,
state_root: B256,
block_access_list_hash: Option<B256>,
) -> Self {
Self {
evm_env,
@@ -232,6 +246,7 @@ impl<'a, 'b, F: BlockExecutorFactory, H> BlockAssemblerInput<'a, 'b, F, H> {
bundle_state,
state_provider,
state_root,
block_access_list_hash,
}
}
}
@@ -301,6 +316,8 @@ pub struct BlockBuilderOutcome<N: NodePrimitives> {
pub trie_updates: TrieUpdates,
/// The built block.
pub block: RecoveredBlock<N::Block>,
/// Block access list built during execution (EIP-7928, Amsterdam).
pub block_access_list: Option<BlockAccessList>,
}
/// A type that knows how to execute and build a block.
@@ -327,7 +344,7 @@ pub trait BlockBuilder {
&mut self,
tx: impl ExecutorTx<Self::Executor>,
f: impl FnOnce(&<Self::Executor as BlockExecutor>::Result) -> CommitChanges,
) -> Result<Option<u64>, BlockExecutionError>;
) -> Result<Option<GasOutput>, BlockExecutionError>;
/// Invokes [`BlockExecutor::execute_transaction_with_result_closure`] and saves the
/// transaction in internal state.
@@ -335,7 +352,7 @@ pub trait BlockBuilder {
&mut self,
tx: impl ExecutorTx<Self::Executor>,
f: impl FnOnce(&<Self::Executor as BlockExecutor>::Result),
) -> Result<u64, BlockExecutionError> {
) -> Result<GasOutput, BlockExecutionError> {
self.execute_transaction_with_commit_condition(tx, |res| {
f(res);
CommitChanges::Yes
@@ -348,7 +365,7 @@ pub trait BlockBuilder {
fn execute_transaction(
&mut self,
tx: impl ExecutorTx<Self::Executor>,
) -> Result<u64, BlockExecutionError> {
) -> Result<GasOutput, BlockExecutionError> {
self.execute_transaction_with_result_closure(tx, |_| ())
}
@@ -453,20 +470,26 @@ where
type Executor = Executor;
fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
self.executor.apply_pre_execution_changes()
self.executor.apply_pre_execution_changes()?;
// Bump BAL index after pre-execution changes (EIP-7928: index 0 is pre-execution)
self.executor.evm_mut().db_mut().bump_bal_index();
Ok(())
}
fn execute_transaction_with_commit_condition(
&mut self,
tx: impl ExecutorTx<Self::Executor>,
f: impl FnOnce(&<Self::Executor as BlockExecutor>::Result) -> CommitChanges,
) -> Result<Option<u64>, BlockExecutionError> {
) -> Result<Option<GasOutput>, BlockExecutionError> {
let (tx_env, tx) = tx.into_parts();
if let Some(gas_used) =
self.executor.execute_transaction_with_commit_condition((tx_env, &tx), f)?
{
self.transactions.push(tx);
Ok(Some(gas_used.tx_gas_used()))
// Bump BAL index after each committed transaction (EIP-7928)
self.executor.evm_mut().db_mut().bump_bal_index();
Ok(Some(gas_used))
} else {
Ok(None)
}
@@ -483,6 +506,11 @@ where
// merge all transitions into bundle state
db.merge_transitions(BundleRetention::Reverts);
// extract the built block access list (EIP-7928, Amsterdam) and compute its hash
let block_access_list = db.take_built_alloy_bal();
let block_access_list_hash =
block_access_list.as_ref().map(|bal| compute_block_access_list_hash(bal));
let hashed_state = state.hashed_post_state(&db.bundle_state);
let (state_root, trie_updates) = match state_root_precomputed {
Some(precomputed) => precomputed,
@@ -503,11 +531,18 @@ where
bundle_state: &db.bundle_state,
state_provider: &state,
state_root,
block_access_list_hash,
})?;
let block = RecoveredBlock::new_unhashed(block, senders);
Ok(BlockBuilderOutcome { execution_result: result, hashed_state, trie_updates, block })
Ok(BlockBuilderOutcome {
execution_result: result,
hashed_state,
trie_updates,
block,
block_access_list,
})
}
fn executor_mut(&mut self) -> &mut Self::Executor {
@@ -554,11 +589,33 @@ where
block: &RecoveredBlock<<Self::Primitives as NodePrimitives>::Block>,
) -> Result<BlockExecutionResult<<Self::Primitives as NodePrimitives>::Receipt>, Self::Error>
{
let result = self
let mut executor = self
.strategy_factory
.executor_for_block(&mut self.db, block)
.map_err(BlockExecutionError::other)?
.execute_block(block.transactions_recovered())?;
.map_err(BlockExecutionError::other)?;
let has_bal = block.header().block_access_list_hash().is_some();
if has_bal {
executor.evm_mut().db_mut().bal_state.bal_builder = Some(Bal::new());
} else {
executor.evm_mut().db_mut().bal_state.bal_builder = None;
}
executor.apply_pre_execution_changes()?;
if has_bal {
executor.evm_mut().db_mut().bump_bal_index();
}
for tx in block.transactions_recovered() {
executor.execute_transaction(tx)?;
if has_bal {
executor.evm_mut().db_mut().bump_bal_index();
}
}
let result = executor.apply_post_execution_changes()?;
self.db.merge_transitions(BundleRetention::Reverts);
@@ -592,6 +649,10 @@ where
fn size_hint(&self) -> usize {
self.db.bundle_state.size_hint()
}
fn take_bal(&mut self) -> Option<BlockAccessList> {
self.db.take_built_alloy_bal()
}
}
/// A helper trait marking a 'static type that can be converted into an [`ExecutableTxParts`] for
@@ -697,6 +758,10 @@ mod tests {
fn size_hint(&self) -> usize {
0
}
fn take_bal(&mut self) -> Option<BlockAccessList> {
None
}
}
#[test]

View File

@@ -312,7 +312,7 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin {
&'a self,
evm: EvmFor<Self, &'a mut State<DB>, I>,
ctx: <Self::BlockExecutorFactory as BlockExecutorFactory>::ExecutionCtx<'a>,
) -> impl BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>, I>
) -> BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>, I>
where
DB: Database,
I: InspectorFor<Self, &'a mut State<DB>> + 'a,
@@ -325,7 +325,7 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin {
&'a self,
db: &'a mut State<DB>,
block: &'a SealedBlock<<Self::Primitives as NodePrimitives>::Block>,
) -> Result<impl BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>>, Self::Error>
) -> Result<BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>>, Self::Error>
{
let evm = self.evm_for_block(db, block.header())?;
let ctx = self.context_for_block(block)?;
@@ -354,7 +354,7 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin {
ctx: <Self::BlockExecutorFactory as BlockExecutorFactory>::ExecutionCtx<'a>,
) -> impl BlockBuilder<
Primitives = Self::Primitives,
Executor: BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>, I>,
Executor = BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>, I>,
>
where
DB: Database,
@@ -406,7 +406,7 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin {
) -> Result<
impl BlockBuilder<
Primitives = Self::Primitives,
Executor: BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>>,
Executor = BlockExecutorFor<'a, Self::BlockExecutorFactory, &'a mut State<DB>>,
>,
Self::Error,
> {

View File

@@ -25,6 +25,7 @@ alloy-eips.workspace = true
alloy-primitives = { workspace = true, features = ["map"] }
alloy-rlp = { workspace = true, features = ["derive"] }
alloy-consensus.workspace = true
alloy-trie.workspace = true
bytes.workspace = true
derive_more.workspace = true
@@ -62,6 +63,7 @@ std = [
"alloy-rlp/std",
"bytes/std",
"derive_more/std",
"alloy-trie/std",
"reth-ethereum-primitives/std",
"reth-primitives-traits/std",
"serde?/std",
@@ -80,6 +82,7 @@ arbitrary = [
"alloy-consensus/arbitrary",
"alloy-eips/arbitrary",
"alloy-primitives/arbitrary",
"alloy-trie/arbitrary",
"reth-primitives-traits/arbitrary",
]
serde = [
@@ -88,6 +91,7 @@ serde = [
"alloy-consensus/serde",
"alloy-eips/serde",
"alloy-primitives/serde",
"alloy-trie/serde",
"bytes/serde",
"rand/serde",
"reth-primitives-traits/serde",

View File

@@ -23,8 +23,9 @@ pub struct GetBlockAccessLists(
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[add_arbitrary_tests(rlp)]
pub struct BlockAccessLists(
/// The requested block access lists as raw RLP blobs. Per EIP-8159, unavailable entries are
/// represented by an RLP-encoded empty list (`0xc0`).
/// The requested block access lists as raw RLP blobs. Missing-entry encoding is protocol
/// specific: eth/71 uses the RLP empty list (`0xc0`), while snap/2 uses the RLP empty string
/// (`0x80`).
pub Vec<Bytes>,
);
@@ -57,9 +58,6 @@ impl Decodable for BlockAccessLists {
while !payload.is_empty() {
let item_start = payload;
let item_header = Header::decode(&mut payload)?;
if !item_header.list {
return Err(alloy_rlp::Error::UnexpectedString)
}
let item_length = item_header.length_with_payload();
bals.push(Bytes::copy_from_slice(&item_start[..item_length]));
@@ -171,9 +169,14 @@ mod tests {
}
#[test]
fn rejects_non_list_bal_entries() {
let err = alloy_rlp::decode_exact::<BlockAccessLists>(&[0xc1, 0x01]).unwrap_err();
assert!(matches!(err, alloy_rlp::Error::UnexpectedString));
fn accepts_snap_missing_bal_entries() {
let decoded =
alloy_rlp::decode_exact::<BlockAccessLists>(&[0xc1, alloy_rlp::EMPTY_STRING_CODE])
.unwrap();
assert_eq!(
decoded,
BlockAccessLists(vec![Bytes::from_static(&[alloy_rlp::EMPTY_STRING_CODE])])
);
}
#[test]

View File

@@ -7,13 +7,14 @@ use alloy_primitives::{
Bytes, TxHash, B256, U128,
};
use alloy_rlp::{
Decodable, Encodable, RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper,
Decodable, Encodable, Header, RlpDecodable, RlpDecodableWrapper, RlpEncodable,
RlpEncodableWrapper,
};
use core::{fmt::Debug, mem};
use derive_more::{Constructor, Deref, DerefMut, From, IntoIterator};
use reth_codecs_derive::{add_arbitrary_tests, generate_tests};
use reth_ethereum_primitives::TransactionSigned;
use reth_primitives_traits::{Block, SignedTransaction};
use reth_primitives_traits::{Block, InMemorySize, SignedTransaction};
/// This informs peers of new blocks that have appeared on the network.
#[derive(
@@ -143,6 +144,53 @@ impl<T> From<Transactions<T>> for Vec<T> {
}
}
impl<T: Decodable + InMemorySize> Transactions<T> {
/// Decodes the RLP list of transactions, stopping once the cumulative
/// [`InMemorySize`] of decoded transactions exceeds `memory_budget` bytes.
/// Any remaining transactions in the payload are skipped.
pub fn decode_with_memory_budget(
buf: &mut &[u8],
memory_budget: usize,
) -> alloy_rlp::Result<Self> {
decode_list_with_memory_budget(buf, memory_budget).map(Self)
}
}
/// Decodes an RLP list, stopping once the cumulative [`InMemorySize`] of decoded items exceeds
/// `memory_budget` bytes. Any remaining items in the payload are skipped.
pub fn decode_list_with_memory_budget<T: Decodable + InMemorySize>(
buf: &mut &[u8],
memory_budget: usize,
) -> alloy_rlp::Result<Vec<T>> {
let header = Header::decode(buf)?;
if !header.list {
return Err(alloy_rlp::Error::UnexpectedString);
}
if buf.len() < header.payload_length {
return Err(alloy_rlp::Error::InputTooShort);
}
let (payload, rest) = buf.split_at(header.payload_length);
let mut payload = payload;
let mut txs = Vec::new();
let mut total_size = 0usize;
while !payload.is_empty() {
let item = T::decode(&mut payload)?;
total_size = total_size.saturating_add(item.size());
if total_size > memory_budget {
break;
}
txs.push(item);
}
*buf = rest;
Ok(txs)
}
/// Same as [`Transactions`] but this is intended as egress message send from local to _many_ peers.
///
/// The list of transactions is constructed on per-peers basis, but the underlying transaction

View File

@@ -120,11 +120,6 @@ impl Capability {
Self::eth(EthVersion::Eth71)
}
/// Returns the `snap/1` capability.
pub const fn snap_1() -> Self {
Self::snap(SnapVersion::V1)
}
/// Returns the `snap/2` capability.
pub const fn snap_2() -> Self {
Self::snap(SnapVersion::V2)
@@ -176,6 +171,18 @@ impl Capability {
self.is_eth_v70() ||
self.is_eth_v71()
}
/// Whether this is snap v2.
#[inline]
pub fn is_snap_v2(&self) -> bool {
self.name == "snap" && self.version == 2
}
/// Whether this is any snap version.
#[inline]
pub fn is_snap(&self) -> bool {
self.is_snap_v2()
}
}
impl fmt::Display for Capability {
@@ -211,6 +218,7 @@ pub struct Capabilities {
eth_69: bool,
eth_70: bool,
eth_71: bool,
snap_2: bool,
}
impl Capabilities {
@@ -223,6 +231,7 @@ impl Capabilities {
eth_69: value.iter().any(Capability::is_eth_v69),
eth_70: value.iter().any(Capability::is_eth_v70),
eth_71: value.iter().any(Capability::is_eth_v71),
snap_2: value.iter().any(Capability::is_snap_v2),
inner: value,
}
}
@@ -309,6 +318,20 @@ impl Capabilities {
pub const fn supports_eth_v71(&self) -> bool {
self.eth_71
}
/// Whether this peer supports snap v2.
#[inline]
pub const fn supports_snap_v2(&self) -> bool {
self.snap_2
}
/// Returns true if this peer advertises the requested snap protocol version.
#[inline]
pub const fn supports_snap_version(&self, version: SnapVersion) -> bool {
match version {
SnapVersion::V2 => self.snap_2,
}
}
}
impl From<Vec<Capability>> for Capabilities {
@@ -334,6 +357,7 @@ impl Decodable for Capabilities {
eth_69: inner.iter().any(Capability::is_eth_v69),
eth_70: inner.iter().any(Capability::is_eth_v70),
eth_71: inner.iter().any(Capability::is_eth_v71),
snap_2: inner.iter().any(Capability::is_snap_v2),
inner,
})
}

View File

@@ -28,6 +28,15 @@ use core::fmt::Debug;
// https://github.com/ethereum/go-ethereum/blob/30602163d5d8321fbc68afdcbbaf2362b2641bde/eth/protocols/eth/protocol.go#L50
pub const MAX_MESSAGE_SIZE: usize = 10 * 1024 * 1024;
/// Multiplier applied to `max_message_size` to derive the in-memory budget for decoding
/// `Transactions` and `PooledTransactions` messages.
///
/// Decoded transactions expand relative to their RLP encoding due to struct overhead and heap
/// allocations. With many peers in flight this can cause significant memory pressure, so we
/// stop decoding once the cumulative in-memory size of decoded transactions exceeds
/// `max_message_size * TX_MEMORY_BUDGET_MULTIPLIER`. Remaining transactions are silently dropped.
pub const TX_MEMORY_BUDGET_MULTIPLIER: usize = 2;
/// Error when sending/receiving a message
#[derive(thiserror::Error, Debug)]
pub enum MessageError {
@@ -87,6 +96,19 @@ impl<N: NetworkPrimitives> ProtocolMessage<N> {
///
/// This will enforce decoding according to the given [`EthVersion`] of the connection.
pub fn decode_message(version: EthVersion, buf: &mut &[u8]) -> Result<Self, MessageError> {
Self::decode_message_with_tx_memory_budget(version, buf, usize::MAX)
}
/// Like [`Self::decode_message`], but caps the cumulative in-memory size of decoded
/// transactions in `Transactions` and `PooledTransactions` messages. Once exceeded,
/// remaining transactions are silently dropped.
///
/// Use [`TX_MEMORY_BUDGET_MULTIPLIER`] to derive a reasonable default.
pub fn decode_message_with_tx_memory_budget(
version: EthVersion,
buf: &mut &[u8],
tx_memory_budget: usize,
) -> Result<Self, MessageError> {
let message_type = EthMessageID::decode(buf)?;
// For EIP-7642 (https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7642.md):
@@ -103,7 +125,9 @@ impl<N: NetworkPrimitives> ProtocolMessage<N> {
EthMessageID::NewBlock => {
EthMessage::NewBlock(Box::new(N::NewBlockPayload::decode(buf)?))
}
EthMessageID::Transactions => EthMessage::Transactions(Transactions::decode(buf)?),
EthMessageID::Transactions => EthMessage::Transactions(
Transactions::decode_with_memory_budget(buf, tx_memory_budget)?,
),
EthMessageID::NewPooledTransactionHashes => {
if version >= EthVersion::Eth68 {
EthMessage::NewPooledTransactionHashes68(NewPooledTransactionHashes68::decode(
@@ -123,7 +147,9 @@ impl<N: NetworkPrimitives> ProtocolMessage<N> {
EthMessage::GetPooledTransactions(RequestPair::decode(buf)?)
}
EthMessageID::PooledTransactions => {
EthMessage::PooledTransactions(RequestPair::decode(buf)?)
EthMessage::PooledTransactions(RequestPair::decode_with(buf, |buf| {
PooledTransactions::decode_with_memory_budget(buf, tx_memory_budget)
})?)
}
EthMessageID::GetNodeData => {
if version >= EthVersion::Eth67 {
@@ -732,6 +758,25 @@ impl<T> RequestPair<T> {
let Self { request_id, message } = self;
RequestPair { request_id, message: f(message) }
}
/// Decodes the request id and then decodes the message payload using `decode_msg`.
pub fn decode_with<F>(buf: &mut &[u8], decode_msg: F) -> alloy_rlp::Result<Self>
where
F: FnOnce(&mut &[u8]) -> alloy_rlp::Result<T>,
{
let header = Header::decode(buf)?;
let initial_length = buf.len();
let request_id = u64::decode(buf)?;
let message = decode_msg(buf)?;
let consumed_len = initial_length - buf.len();
if consumed_len != header.payload_length {
return Err(alloy_rlp::Error::UnexpectedLength)
}
Ok(Self { request_id, message })
}
}
/// Allows messages with request ids to be serialized into RLP bytes.
@@ -784,11 +829,11 @@ where
mod tests {
use super::MessageError;
use crate::{
message::RequestPair, BlockAccessLists, EthMessage, EthMessageID, EthNetworkPrimitives,
EthVersion, GetBlockAccessLists, GetNodeData, NodeData, ProtocolMessage,
RawCapabilityMessage,
message::RequestPair, BlockAccessLists, BlockRangeUpdate, EthMessage, EthMessageID,
EthNetworkPrimitives, EthVersion, GetBlockAccessLists, GetNodeData, NodeData,
ProtocolMessage, RawCapabilityMessage,
};
use alloy_primitives::hex;
use alloy_primitives::{hex, B256};
use alloy_rlp::{Decodable, Encodable, Error};
use reth_ethereum_primitives::BlockBody;
@@ -829,6 +874,25 @@ mod tests {
#[test]
fn test_bal_message_version_gating() {
let block_range_update =
EthMessage::<EthNetworkPrimitives>::BlockRangeUpdate(BlockRangeUpdate {
earliest: 1,
latest: 2,
latest_hash: B256::random(),
});
let buf = encode(ProtocolMessage {
message_type: EthMessageID::BlockRangeUpdate,
message: block_range_update,
});
let msg = ProtocolMessage::<EthNetworkPrimitives>::decode_message(
EthVersion::Eth68,
&mut &buf[..],
);
assert!(matches!(
msg,
Err(MessageError::Invalid(EthVersion::Eth68, EthMessageID::BlockRangeUpdate))
));
let get_block_access_lists =
EthMessage::<EthNetworkPrimitives>::GetBlockAccessLists(RequestPair {
request_id: 1337,

View File

@@ -3,12 +3,16 @@
//! facilitating the exchange of Ethereum state snapshots between peers
//! Reference: [Ethereum Snapshot Protocol](https://github.com/ethereum/devp2p/blob/master/caps/snap.md#protocol-messages)
//!
//! This module currently includes snap/1 plus preparatory snap/2 message definitions.
//! This module currently includes the snap/2 message definitions used by this branch.
use crate::BlockAccessLists;
use alloc::vec::Vec;
use alloy_primitives::{Bytes, B256};
use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
use alloy_primitives::{bytes::Buf, Bytes, B256, U256};
use alloy_rlp::{
BufMut, Decodable, Encodable, Header, RlpDecodable, RlpEncodable, EMPTY_LIST_CODE,
};
pub use alloy_trie::TrieAccount;
use alloy_trie::{EMPTY_ROOT_HASH, KECCAK_EMPTY};
use reth_codecs_derive::add_arbitrary_tests;
/// Supported SNAP protocol versions.
@@ -16,10 +20,8 @@ use reth_codecs_derive::add_arbitrary_tests;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum SnapVersion {
/// The original snapshot protocol.
#[default]
V1 = 1,
/// BAL-based healing as proposed by EIP-8189.
#[default]
V2 = 2,
}
@@ -27,7 +29,6 @@ impl SnapVersion {
/// Returns the number of messages supported by this version.
pub const fn message_count(self) -> u8 {
match self {
Self::V1 => 8,
Self::V2 => 10,
}
}
@@ -54,24 +55,49 @@ pub enum SnapMessageId {
GetByteCodes = 0x04,
/// Response for the number of requested contract codes.
ByteCodes = 0x05,
/// Request of the number of state (either account or storage) Merkle trie nodes by path.
///
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
GetTrieNodes = 0x06,
/// Response for the number of requested state trie nodes.
///
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
TrieNodes = 0x07,
/// Request BALs for a list of block hashes.
///
/// Only valid for `snap/2`.
GetBlockAccessLists = 0x08,
/// Response containing BALs for the requested block hashes.
///
/// Only valid for `snap/2`.
BlockAccessLists = 0x09,
}
impl SnapMessageId {
/// Returns true if this message id is valid for snap/2.
pub const fn is_valid_for_version(self, version: SnapVersion) -> bool {
match version {
SnapVersion::V2 => matches!(
self,
Self::GetAccountRange |
Self::AccountRange |
Self::GetStorageRanges |
Self::StorageRanges |
Self::GetByteCodes |
Self::ByteCodes |
Self::GetBlockAccessLists |
Self::BlockAccessLists
),
}
}
}
impl TryFrom<u8> for SnapMessageId {
type Error = alloy_rlp::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x00 => Ok(Self::GetAccountRange),
0x01 => Ok(Self::AccountRange),
0x02 => Ok(Self::GetStorageRanges),
0x03 => Ok(Self::StorageRanges),
0x04 => Ok(Self::GetByteCodes),
0x05 => Ok(Self::ByteCodes),
0x08 => Ok(Self::GetBlockAccessLists),
0x09 => Ok(Self::BlockAccessLists),
_ => Err(alloy_rlp::Error::Custom("Unknown message ID")),
}
}
}
/// Request for a range of accounts from the state trie.
// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getaccountrange-0x00
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
@@ -91,14 +117,119 @@ pub struct GetAccountRangeMessage {
}
/// Account data in the response.
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[add_arbitrary_tests(rlp)]
pub struct AccountData {
/// Hash of the account address (trie path)
pub hash: B256,
/// Account body in slim format
pub body: Bytes,
/// Account trie value.
pub account: TrieAccount,
}
impl Encodable for AccountData {
fn encode(&self, out: &mut dyn BufMut) {
self.as_wire().encode(out);
}
fn length(&self) -> usize {
self.as_wire().length()
}
}
impl Decodable for AccountData {
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
AccountDataWire::decode(buf).and_then(TryInto::try_into)
}
}
impl AccountData {
/// Returns the encoded byte length of this account's snap slim body.
pub fn account_body_len(account: TrieAccount) -> usize {
snap_account_payload_length(&account).length_with_payload()
}
fn as_wire(&self) -> AccountDataWire {
AccountDataWire { hash: self.hash, body: encode_account_body(self.account).into() }
}
}
#[derive(RlpEncodable, RlpDecodable)]
struct AccountDataWire {
hash: B256,
body: Bytes,
}
impl TryFrom<AccountDataWire> for AccountData {
type Error = alloy_rlp::Error;
fn try_from(value: AccountDataWire) -> Result<Self, Self::Error> {
let account = decode_account_body(&value.body)?;
Ok(Self { hash: value.hash, account })
}
}
fn encode_account_body(account: TrieAccount) -> Vec<u8> {
let mut out = Vec::with_capacity(AccountData::account_body_len(account));
snap_account_payload_length(&account).encode(&mut out);
account.nonce.encode(&mut out);
account.balance.encode(&mut out);
encode_slim_hash(account.storage_root, EMPTY_ROOT_HASH, &mut out);
encode_slim_hash(account.code_hash, KECCAK_EMPTY, &mut out);
out
}
fn decode_account_body(mut buf: &[u8]) -> alloy_rlp::Result<TrieAccount> {
let header = Header::decode(&mut buf)?;
if !header.list {
return Err(alloy_rlp::Error::UnexpectedString)
}
let initial_len = buf.len();
let nonce = u64::decode(&mut buf)?;
let balance = U256::decode(&mut buf)?;
let storage_root = decode_slim_hash(&mut buf, EMPTY_ROOT_HASH)?;
let code_hash = decode_slim_hash(&mut buf, KECCAK_EMPTY)?;
let consumed = initial_len - buf.len();
if consumed != header.payload_length || !buf.is_empty() {
return Err(alloy_rlp::Error::UnexpectedLength)
}
Ok(TrieAccount { nonce, balance, storage_root, code_hash })
}
fn snap_account_payload_length(account: &TrieAccount) -> Header {
let payload_length = account.nonce.length() +
account.balance.length() +
slim_hash_length(account.storage_root, EMPTY_ROOT_HASH) +
slim_hash_length(account.code_hash, KECCAK_EMPTY);
Header { list: true, payload_length }
}
fn slim_hash_length(hash: B256, empty_hash: B256) -> usize {
if hash == empty_hash {
1
} else {
hash.length()
}
}
fn encode_slim_hash(hash: B256, empty_hash: B256, out: &mut dyn BufMut) {
if hash == empty_hash {
out.put_u8(EMPTY_LIST_CODE);
} else {
hash.encode(out);
}
}
fn decode_slim_hash(buf: &mut &[u8], empty_hash: B256) -> alloy_rlp::Result<B256> {
if buf.first().copied() == Some(EMPTY_LIST_CODE) {
buf.advance(1);
Ok(empty_hash)
} else {
B256::decode(buf)
}
}
/// Response containing a number of consecutive accounts and the Merkle proofs for the entire range.
@@ -146,6 +277,25 @@ pub struct StorageData {
pub data: Bytes,
}
impl StorageData {
/// Creates storage data from a decoded storage value.
pub fn from_value(hash: B256, value: U256) -> Self {
let mut data = Vec::new();
value.encode(&mut data);
Self { hash, data: data.into() }
}
/// Decodes this slot's RLP-encoded storage value.
pub fn decode_value(&self) -> alloy_rlp::Result<U256> {
let mut buf = self.data.as_ref();
let value = U256::decode(&mut buf)?;
if !buf.is_empty() {
return Err(alloy_rlp::Error::Custom("trailing bytes after storage value"))
}
Ok(value)
}
}
/// Response containing a number of consecutive storage slots for the requested account
/// and optionally the merkle proofs for the last range (boundary proofs) if it only partially
/// covers the storage trie.
@@ -188,45 +338,6 @@ pub struct ByteCodesMessage {
pub codes: Vec<Bytes>,
}
/// Path in the trie for an account and its storage
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[add_arbitrary_tests(rlp)]
pub struct TriePath {
/// Path in the account trie
pub account_path: Bytes,
/// Paths in the storage trie
pub slot_paths: Vec<Bytes>,
}
/// Request a number of state (either account or storage) Merkle trie nodes by path
// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#gettrienodes-0x06
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[add_arbitrary_tests(rlp)]
pub struct GetTrieNodesMessage {
/// Request ID to match up responses with
pub request_id: u64,
/// Root hash of the account trie to serve
pub root_hash: B256,
/// Trie paths to retrieve the nodes for, grouped by account
pub paths: Vec<TriePath>,
/// Soft limit at which to stop returning data (in bytes)
pub response_bytes: u64,
}
/// Response containing a number of requested state trie nodes
// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#trienodes-0x07
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
#[add_arbitrary_tests(rlp)]
pub struct TrieNodesMessage {
/// ID of the request this is a response for
pub request_id: u64,
/// The requested trie nodes in order
pub nodes: Vec<Bytes>,
}
/// Request BALs for the given block hashes.
#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
@@ -266,21 +377,9 @@ pub enum SnapProtocolMessage {
GetByteCodes(GetByteCodesMessage),
/// Response with contract codes - see [`ByteCodesMessage`]
ByteCodes(ByteCodesMessage),
/// Request for trie nodes - see [`GetTrieNodesMessage`]
///
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
GetTrieNodes(GetTrieNodesMessage),
/// Response with trie nodes - see [`TrieNodesMessage`]
///
/// Only valid for `snap/1`. Replaced by BAL-based healing in `snap/2`.
TrieNodes(TrieNodesMessage),
/// Request for block access lists - see [`GetBlockAccessListsMessage`]
///
/// Only valid for `snap/2`.
GetBlockAccessLists(GetBlockAccessListsMessage),
/// Response with block access lists - see [`BlockAccessListsMessage`]
///
/// Only valid for `snap/2`.
BlockAccessLists(BlockAccessListsMessage),
}
@@ -296,8 +395,6 @@ impl SnapProtocolMessage {
Self::StorageRanges(_) => SnapMessageId::StorageRanges,
Self::GetByteCodes(_) => SnapMessageId::GetByteCodes,
Self::ByteCodes(_) => SnapMessageId::ByteCodes,
Self::GetTrieNodes(_) => SnapMessageId::GetTrieNodes,
Self::TrieNodes(_) => SnapMessageId::TrieNodes,
Self::GetBlockAccessLists(_) => SnapMessageId::GetBlockAccessLists,
Self::BlockAccessLists(_) => SnapMessageId::BlockAccessLists,
}
@@ -317,8 +414,6 @@ impl SnapProtocolMessage {
Self::StorageRanges(msg) => msg.encode(&mut buf),
Self::GetByteCodes(msg) => msg.encode(&mut buf),
Self::ByteCodes(msg) => msg.encode(&mut buf),
Self::GetTrieNodes(msg) => msg.encode(&mut buf),
Self::TrieNodes(msg) => msg.encode(&mut buf),
Self::GetBlockAccessLists(msg) => msg.encode(&mut buf),
Self::BlockAccessLists(msg) => msg.encode(&mut buf),
}
@@ -328,6 +423,26 @@ impl SnapProtocolMessage {
/// Decodes a SNAP protocol message from its message ID and RLP-encoded body.
pub fn decode(message_id: u8, buf: &mut &[u8]) -> Result<Self, alloy_rlp::Error> {
Self::decode_unchecked(message_id, buf)
}
/// Decodes a SNAP protocol message for the negotiated snap protocol version.
pub fn decode_with_version(
version: SnapVersion,
message_id: u8,
buf: &mut &[u8],
) -> Result<Self, alloy_rlp::Error> {
let id = SnapMessageId::try_from(message_id)?;
if !id.is_valid_for_version(version) {
return Err(alloy_rlp::Error::Custom("Invalid message ID for snap version"));
}
Self::decode_unchecked(message_id, buf)
}
fn decode_unchecked(message_id: u8, buf: &mut &[u8]) -> Result<Self, alloy_rlp::Error> {
let _ = SnapMessageId::try_from(message_id)?;
// Decoding protocol message variants based on message ID
macro_rules! decode_snap_message_variant {
($message_id:expr, $buf:expr, $id:expr, $variant:ident, $msg_type:ty) => {
@@ -380,20 +495,6 @@ impl SnapProtocolMessage {
ByteCodes,
ByteCodesMessage
);
decode_snap_message_variant!(
message_id,
buf,
SnapMessageId::GetTrieNodes,
GetTrieNodes,
GetTrieNodesMessage
);
decode_snap_message_variant!(
message_id,
buf,
SnapMessageId::TrieNodes,
TrieNodes,
TrieNodesMessage
);
decode_snap_message_variant!(
message_id,
buf,
@@ -438,7 +539,6 @@ mod tests {
#[test]
fn test_all_message_roundtrips() {
assert_eq!(SnapVersion::V1.message_count(), 8);
assert_eq!(SnapVersion::V2.message_count(), 10);
test_roundtrip(SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
@@ -453,7 +553,12 @@ mod tests {
request_id: 42,
accounts: vec![AccountData {
hash: b256_from_u64(123),
body: Bytes::from(vec![1, 2, 3]),
account: TrieAccount {
nonce: 7,
balance: U256::from(42),
storage_root: b256_from_u64(456),
code_hash: b256_from_u64(789),
},
}],
proof: vec![Bytes::from(vec![4, 5, 6])],
}));
@@ -487,21 +592,6 @@ mod tests {
codes: vec![Bytes::from(vec![1, 2, 3])],
}));
test_roundtrip(SnapProtocolMessage::GetTrieNodes(GetTrieNodesMessage {
request_id: 42,
root_hash: b256_from_u64(123),
paths: vec![TriePath {
account_path: Bytes::from(vec![1, 2, 3]),
slot_paths: vec![Bytes::from(vec![4, 5, 6])],
}],
response_bytes: 1024,
}));
test_roundtrip(SnapProtocolMessage::TrieNodes(TrieNodesMessage {
request_id: 42,
nodes: vec![Bytes::from(vec![1, 2, 3])],
}));
test_roundtrip(SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
request_id: 42,
block_hashes: vec![b256_from_u64(123), b256_from_u64(456)],
@@ -530,5 +620,61 @@ mod tests {
if let Err(e) = result {
assert_eq!(e.to_string(), "Unknown message ID");
}
for removed_id in [0x06, 0x07] {
let mut buf = data.as_ref();
let result = SnapProtocolMessage::decode(removed_id, &mut buf);
assert!(result.is_err());
if let Err(e) = result {
assert_eq!(e.to_string(), "Unknown message ID");
}
}
}
#[test]
fn storage_data_decodes_value() {
let storage = StorageData::from_value(b256_from_u64(1), U256::from(99));
assert_eq!(storage.decode_value().unwrap(), U256::from(99));
}
#[test]
fn snap_account_uses_empty_list_sentinels() {
let account = TrieAccount {
nonce: 1,
balance: U256::from(2),
storage_root: alloy_trie::EMPTY_ROOT_HASH,
code_hash: alloy_trie::KECCAK_EMPTY,
};
let encoded = encode_account_body(account);
assert_eq!(
encoded,
vec![0xc4, 0x01, 0x02, alloy_rlp::EMPTY_LIST_CODE, alloy_rlp::EMPTY_LIST_CODE]
);
assert_eq!(decode_account_body(&encoded).unwrap(), account);
}
#[test]
fn account_data_encodes_snap_account_body() {
let data = AccountData {
hash: b256_from_u64(1),
account: TrieAccount {
nonce: 1,
balance: U256::from(2),
storage_root: alloy_trie::EMPTY_ROOT_HASH,
code_hash: alloy_trie::KECCAK_EMPTY,
},
};
let encoded = alloy_rlp::encode(data.clone());
let decoded = alloy_rlp::decode_exact::<AccountData>(&encoded).unwrap();
assert_eq!(decoded, data);
let wire = AccountDataWire::decode(&mut &encoded[..]).unwrap();
assert_eq!(
wire.body.as_ref(),
&[0xc4, 0x01, 0x02, alloy_rlp::EMPTY_LIST_CODE, alloy_rlp::EMPTY_LIST_CODE]
);
}
}

View File

@@ -1,12 +1,14 @@
//! Implements the `GetPooledTransactions` and `PooledTransactions` message types.
use crate::broadcast::decode_list_with_memory_budget;
use alloc::vec::Vec;
use alloy_consensus::transaction::PooledTransaction;
use alloy_eips::eip2718::Encodable2718;
use alloy_primitives::B256;
use alloy_rlp::{RlpDecodableWrapper, RlpEncodableWrapper};
use alloy_rlp::{Decodable, RlpDecodableWrapper, RlpEncodableWrapper};
use derive_more::{Constructor, Deref, IntoIterator};
use reth_codecs_derive::add_arbitrary_tests;
use reth_primitives_traits::InMemorySize;
/// A list of transaction hashes that the peer would like transaction bodies for.
#[derive(
@@ -62,6 +64,18 @@ pub struct PooledTransactions<T = PooledTransaction>(
pub Vec<T>,
);
impl<T: Decodable + InMemorySize> PooledTransactions<T> {
/// Decodes the RLP list of transactions, stopping once the cumulative
/// [`InMemorySize`] of decoded transactions exceeds `memory_budget` bytes.
/// Any remaining transactions in the payload are skipped.
pub fn decode_with_memory_budget(
buf: &mut &[u8],
memory_budget: usize,
) -> alloy_rlp::Result<Self> {
decode_list_with_memory_budget(buf, memory_budget).map(Self)
}
}
impl<T: Encodable2718> PooledTransactions<T> {
/// Returns an iterator over the transaction hashes in this response.
pub fn hashes(&self) -> impl Iterator<Item = B256> + '_ {

View File

@@ -35,10 +35,11 @@ pub enum EthVersion {
impl EthVersion {
/// The latest known eth version
pub const LATEST: Self = Self::Eth69;
pub const LATEST: Self = Self::Eth70;
/// All known eth versions
pub const ALL_VERSIONS: &'static [Self] = &[Self::Eth69, Self::Eth68, Self::Eth67, Self::Eth66];
pub const ALL_VERSIONS: &'static [Self] =
&[Self::Eth71, Self::Eth70, Self::Eth69, Self::Eth68, Self::Eth67, Self::Eth66];
/// Returns true if the version is eth/66
pub const fn is_eth66(&self) -> bool {

View File

@@ -5,7 +5,7 @@
use super::message::MAX_MESSAGE_SIZE;
use crate::{
message::{EthBroadcastMessage, ProtocolBroadcastMessage},
message::{EthBroadcastMessage, ProtocolBroadcastMessage, TX_MEMORY_BUDGET_MULTIPLIER},
EthMessage, EthMessageID, EthNetworkPrimitives, EthVersion, NetworkPrimitives, ProtocolMessage,
RawCapabilityMessage, SnapProtocolMessage, SnapVersion,
};
@@ -247,7 +247,7 @@ where
{
/// Create a new eth and snap protocol stream
const fn new(eth_version: EthVersion) -> Self {
Self::new_with_snap_version(eth_version, SnapVersion::V1)
Self::new_with_snap_version(eth_version, SnapVersion::V2)
}
/// Create a new eth and snap protocol stream with an explicit snap version.
@@ -257,7 +257,7 @@ where
/// Create a new eth and snap protocol stream with a custom max message size.
const fn with_max_message_size(eth_version: EthVersion, max_message_size: usize) -> Self {
Self::with_max_message_size_and_snap_version(eth_version, SnapVersion::V1, max_message_size)
Self::with_max_message_size_and_snap_version(eth_version, SnapVersion::V2, max_message_size)
}
/// Create a new eth and snap protocol stream with a custom max message size and snap version.
@@ -298,7 +298,11 @@ where
// See also <https://github.com/paradigmxyz/reth/blob/main/crates/net/eth-wire/src/capability.rs#L272-L283>.
if message_id <= EthMessageID::max(self.eth_version) {
let mut buf = bytes.as_ref();
match ProtocolMessage::decode_message(self.eth_version, &mut buf) {
match ProtocolMessage::decode_message_with_tx_memory_budget(
self.eth_version,
&mut buf,
self.max_message_size * TX_MEMORY_BUDGET_MULTIPLIER,
) {
Ok(protocol_msg) => {
if matches!(protocol_msg.message, EthMessage::Status(_)) {
return Err(EthSnapStreamError::StatusNotInHandshake);
@@ -323,7 +327,11 @@ where
let adjusted_message_id = message_id - EthMessageID::message_count(self.eth_version);
let mut buf = &bytes[1..];
match SnapProtocolMessage::decode(adjusted_message_id, &mut buf) {
match SnapProtocolMessage::decode_with_version(
self.snap_version,
adjusted_message_id,
&mut buf,
) {
Ok(snap_msg) => Ok(EthSnapMessage::Snap(snap_msg)),
Err(err) => Err(EthSnapStreamError::Rlp(err)),
}

View File

@@ -7,7 +7,10 @@
use crate::{
errors::{EthHandshakeError, EthStreamError},
handshake::EthereumEthHandshake,
message::{EthBroadcastMessage, EthMessageID, ProtocolBroadcastMessage, MAX_MESSAGE_SIZE},
message::{
EthBroadcastMessage, ProtocolBroadcastMessage, MAX_MESSAGE_SIZE,
TX_MEMORY_BUDGET_MULTIPLIER,
},
p2pstream::HANDSHAKE_TIMEOUT,
CanDisconnect, DisconnectReason, EthMessage, EthNetworkPrimitives, EthVersion, ProtocolMessage,
UnifiedStatus,
@@ -16,7 +19,7 @@ use alloy_primitives::bytes::{Bytes, BytesMut};
use alloy_rlp::Encodable;
use futures::{ready, Sink, SinkExt};
use pin_project::pin_project;
use reth_eth_wire_types::{NetworkPrimitives, RawCapabilityMessage};
use reth_eth_wire_types::{EthMessageID, NetworkPrimitives, RawCapabilityMessage};
use reth_ethereum_forks::ForkFilter;
use std::{
future::Future,
@@ -158,7 +161,11 @@ where
return Err(EthStreamError::UnsupportedMessage { message_id: id });
}
let msg = match ProtocolMessage::decode_message(self.version, &mut bytes.as_ref()) {
let msg = match ProtocolMessage::decode_message_with_tx_memory_budget(
self.version,
&mut bytes.as_ref(),
self.max_message_size * TX_MEMORY_BUDGET_MULTIPLIER,
) {
Ok(m) => m,
Err(err) => {
let msg = if bytes.len() > 50 {

View File

@@ -198,14 +198,17 @@ impl HelloMessageBuilder {
/// Unset fields will be set to their default values:
/// - `protocol_version`: [`ProtocolVersion::V5`]
/// - `client_version`: [`RETH_CLIENT_VERSION`]
/// - `capabilities`: All [`EthVersion`]
/// - `capabilities`: All [`EthVersion`] and snap/2
pub fn build(self) -> HelloMessageWithProtocols {
let Self { protocol_version, client_version, protocols, port, id } = self;
HelloMessageWithProtocols {
protocol_version: protocol_version.unwrap_or_default(),
client_version: client_version.unwrap_or_else(|| RETH_CLIENT_VERSION.to_string()),
protocols: protocols.unwrap_or_else(|| {
EthVersion::ALL_VERSIONS.iter().copied().map(Into::into).collect()
let mut protocols =
EthVersion::ALL_VERSIONS.iter().copied().map(Into::into).collect::<Vec<_>>();
protocols.push(Protocol::snap_2());
protocols
}),
port: port.unwrap_or(DEFAULT_TCP_PORT),
id,
@@ -274,6 +277,9 @@ mod tests {
.iter()
.any(|p| p.cap.name == "eth" && p.cap.version == EthVersion::Eth69 as usize);
assert!(has_eth69, "Default protocols should include Eth69");
let has_snap2 = hello.protocols.iter().any(|p| p.cap == Capability::snap_2());
assert!(has_snap2, "Default protocols should include snap/2");
}
#[test]

View File

@@ -52,11 +52,6 @@ impl Protocol {
Self::eth(EthVersion::Eth68)
}
/// Returns the `snap/1` capability.
pub const fn snap_1() -> Self {
Self::snap(SnapVersion::V1)
}
/// Returns the `snap/2` capability.
pub const fn snap_2() -> Self {
Self::snap(SnapVersion::V2)
@@ -103,7 +98,6 @@ mod tests {
assert_eq!(Protocol::eth(EthVersion::Eth69).messages(), 18);
assert_eq!(Protocol::eth(EthVersion::Eth70).messages(), 18);
assert_eq!(Protocol::eth(EthVersion::Eth71).messages(), 20);
assert_eq!(Protocol::snap(SnapVersion::V1).messages(), 8);
assert_eq!(Protocol::snap(SnapVersion::V2).messages(), 10);
}
}

View File

@@ -1,14 +1,21 @@
//! API related to listening for network events.
use reth_eth_wire_types::{
message::RequestPair, BlockAccessLists, BlockBodies, BlockHeaders, Capabilities,
DisconnectReason, EthMessage, EthNetworkPrimitives, EthVersion, GetBlockAccessLists,
GetBlockBodies, GetBlockHeaders, GetNodeData, GetPooledTransactions, GetReceipts,
GetReceipts70, NetworkPrimitives, NodeData, PooledTransactions, Receipts, Receipts69,
Receipts70, UnifiedStatus,
message::RequestPair,
snap::{
GetAccountRangeMessage, GetBlockAccessListsMessage, GetByteCodesMessage,
GetStorageRangesMessage, SnapProtocolMessage,
},
BlockAccessLists, BlockBodies, BlockHeaders, Capabilities, DisconnectReason, EthMessage,
EthNetworkPrimitives, EthVersion, GetBlockAccessLists, GetBlockBodies, GetBlockHeaders,
GetNodeData, GetPooledTransactions, GetReceipts, GetReceipts70, NetworkPrimitives, NodeData,
PooledTransactions, Receipts, Receipts69, Receipts70, SnapVersion, UnifiedStatus,
};
use reth_ethereum_forks::ForkId;
use reth_network_p2p::error::{RequestError, RequestResult};
use reth_network_p2p::{
error::{RequestError, RequestResult},
snap::client::SnapResponse,
};
use reth_network_peers::{NodeRecord, PeerId};
use reth_network_types::{PeerAddr, PeerKind};
use reth_tokio_util::EventStream;
@@ -262,6 +269,42 @@ pub enum PeerRequest<N: NetworkPrimitives = EthNetworkPrimitives> {
/// The channel to send the response for block access lists.
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
},
/// Requests an account range from the peer (snap protocol).
///
/// The response should be sent through the channel.
GetAccountRange {
/// The request for account range.
request: GetAccountRangeMessage,
/// The channel to send the response for account range.
response: oneshot::Sender<RequestResult<SnapResponse>>,
},
/// Requests storage ranges from the peer (snap protocol).
///
/// The response should be sent through the channel.
GetStorageRanges {
/// The request for storage ranges.
request: GetStorageRangesMessage,
/// The channel to send the response for storage ranges.
response: oneshot::Sender<RequestResult<SnapResponse>>,
},
/// Requests bytecodes from the peer (snap protocol).
///
/// The response should be sent through the channel.
GetByteCodes {
/// The request for bytecodes.
request: GetByteCodesMessage,
/// The channel to send the response for bytecodes.
response: oneshot::Sender<RequestResult<SnapResponse>>,
},
/// Requests block access lists from the peer (snap/2 protocol).
///
/// The response should be sent through the channel.
GetSnapBlockAccessLists {
/// The snap/2 request for block access lists.
request: GetBlockAccessListsMessage,
/// The channel to send the response for block access lists.
response: oneshot::Sender<RequestResult<SnapResponse>>,
},
}
// === impl PeerRequest ===
@@ -283,6 +326,10 @@ impl<N: NetworkPrimitives> PeerRequest<N> {
Self::GetReceipts69 { response, .. } => response.send(Err(err)).ok(),
Self::GetReceipts70 { response, .. } => response.send(Err(err)).ok(),
Self::GetBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
Self::GetAccountRange { response, .. } |
Self::GetStorageRanges { response, .. } |
Self::GetByteCodes { response, .. } |
Self::GetSnapBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
};
}
@@ -295,6 +342,71 @@ impl<N: NetworkPrimitives> PeerRequest<N> {
}
}
/// Returns `true` if this is a snap protocol request.
pub const fn is_snap_request(&self) -> bool {
matches!(
self,
Self::GetAccountRange { .. } |
Self::GetStorageRanges { .. } |
Self::GetByteCodes { .. } |
Self::GetSnapBlockAccessLists { .. }
)
}
/// Returns the required snap protocol version for snap requests.
#[inline]
pub const fn required_snap_version(&self) -> Option<SnapVersion> {
match self {
Self::GetAccountRange { .. } |
Self::GetStorageRanges { .. } |
Self::GetByteCodes { .. } |
Self::GetSnapBlockAccessLists { .. } => Some(SnapVersion::V2),
_ => None,
}
}
/// Creates the [`SnapProtocolMessage`] for snap request types.
///
/// Panics if called on a non-snap request variant.
pub fn create_snap_request_message(&self, request_id: u64) -> SnapProtocolMessage {
match self {
Self::GetAccountRange { request, .. } => {
SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
request_id,
root_hash: request.root_hash,
starting_hash: request.starting_hash,
limit_hash: request.limit_hash,
response_bytes: request.response_bytes,
})
}
Self::GetStorageRanges { request, .. } => {
SnapProtocolMessage::GetStorageRanges(GetStorageRangesMessage {
request_id,
root_hash: request.root_hash,
account_hashes: request.account_hashes.clone(),
starting_hash: request.starting_hash,
limit_hash: request.limit_hash,
response_bytes: request.response_bytes,
})
}
Self::GetByteCodes { request, .. } => {
SnapProtocolMessage::GetByteCodes(GetByteCodesMessage {
request_id,
hashes: request.hashes.clone(),
response_bytes: request.response_bytes,
})
}
Self::GetSnapBlockAccessLists { request, .. } => {
SnapProtocolMessage::GetBlockAccessLists(GetBlockAccessListsMessage {
request_id,
block_hashes: request.block_hashes.clone(),
response_bytes: request.response_bytes,
})
}
_ => unreachable!("create_snap_request_message called on non-snap request"),
}
}
/// Returns the [`EthMessage`] for this type
pub fn create_request_message(&self, request_id: u64) -> EthMessage<N> {
match self {
@@ -325,6 +437,12 @@ impl<N: NetworkPrimitives> PeerRequest<N> {
message: request.clone(),
})
}
Self::GetAccountRange { .. } |
Self::GetStorageRanges { .. } |
Self::GetByteCodes { .. } |
Self::GetSnapBlockAccessLists { .. } => {
unreachable!("snap requests use create_snap_request_message")
}
}
}

View File

@@ -76,6 +76,17 @@ impl<Tx, Eth, N: NetworkPrimitives> NetworkBuilder<Tx, Eth, N> {
NetworkBuilder { network, request_handler, transactions }
}
/// Creates a new [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler) and wires
/// it to the network. Returns the handler which the caller must spawn.
pub fn snap_request_handler<S: reth_network_p2p::snap::server::SnapStateProvider>(
&mut self,
snap_provider: S,
) -> crate::snap_requests::SnapRequestHandler<S> {
let (tx, rx) = mpsc::channel(ETH_REQUEST_CHANNEL_CAPACITY);
self.network.set_snap_request_handler(tx);
crate::snap_requests::SnapRequestHandler::new(snap_provider, rx)
}
/// Creates a new [`TransactionsManager`] and wires it to the network.
pub fn transactions<Pool: TransactionPool>(
self,

View File

@@ -46,6 +46,11 @@ pub const MAX_HEADERS_SERVE: usize = 1024;
/// `SOFT_RESPONSE_LIMIT`.
pub const MAX_BODIES_SERVE: usize = 1024;
/// Maximum number of block access lists to serve.
///
/// Used to limit lookups.
pub const MAX_BLOCK_ACCESS_LISTS_SERVE: usize = 1024;
/// Maximum size of replies to data retrievals: 2MB
pub const SOFT_RESPONSE_LIMIT: usize = 2 * 1024 * 1024;
@@ -323,15 +328,19 @@ where
fn on_block_access_lists_request(
&self,
_peer_id: PeerId,
request: GetBlockAccessLists,
mut request: GetBlockAccessLists,
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
) {
request.0.truncate(MAX_BLOCK_ACCESS_LISTS_SERVE);
let limit = GetBlockAccessListLimit::ResponseSizeSoftLimit(SOFT_RESPONSE_LIMIT);
let access_lists = self
.client
.bal_store()
.get_by_hashes_with_limit(&request.0, limit)
.unwrap_or_else(|_| empty_block_access_lists_with_limit(request.0.len(), limit));
let found = access_lists.iter().filter(|b| b.as_ref() != [0xc0]).count();
tracing::debug!(target: "net::eth", requested=request.0.len(), found, "BAL request received");
let _ = response.send(Ok(BlockAccessLists(access_lists)));
}
}

View File

@@ -4,6 +4,10 @@ use crate::{fetch::DownloadRequest, flattened_response::FlattenedResponse};
use alloy_primitives::B256;
use futures::{future, future::Either};
use reth_eth_wire::{BlockAccessLists, EthNetworkPrimitives, NetworkPrimitives};
use reth_eth_wire_types::snap::{
GetAccountRangeMessage, GetBlockAccessListsMessage, GetByteCodesMessage,
GetStorageRangesMessage,
};
use reth_network_api::test_utils::PeersHandle;
use reth_network_p2p::{
block_access_lists::client::{BalRequirement, BlockAccessListsClient},
@@ -13,6 +17,7 @@ use reth_network_p2p::{
headers::client::{HeadersClient, HeadersRequest},
priority::Priority,
receipts::client::{ReceiptsClient, ReceiptsFut},
snap::client::{SnapClient, SnapResponse},
BlockClient,
};
use reth_network_peers::PeerId;
@@ -126,6 +131,87 @@ impl<N: NetworkPrimitives> BlockClient for FetchClient<N> {
type Block = N::Block;
}
impl<N: NetworkPrimitives> SnapClient for FetchClient<N> {
type Output =
std::pin::Pin<Box<dyn Future<Output = PeerRequestResult<SnapResponse>> + Send + Sync>>;
fn get_account_range_with_priority(
&self,
request: GetAccountRangeMessage,
priority: Priority,
) -> Self::Output {
let (response, rx) = oneshot::channel();
if self
.request_tx
.send(DownloadRequest::GetAccountRange { request, response, priority })
.is_ok()
{
Box::pin(FlattenedResponse::from(rx))
} else {
Box::pin(future::err(RequestError::ChannelClosed))
}
}
fn get_storage_ranges(&self, request: GetStorageRangesMessage) -> Self::Output {
self.get_storage_ranges_with_priority(request, Priority::Normal)
}
fn get_storage_ranges_with_priority(
&self,
request: GetStorageRangesMessage,
priority: Priority,
) -> Self::Output {
let (response, rx) = oneshot::channel();
if self
.request_tx
.send(DownloadRequest::GetStorageRanges { request, response, priority })
.is_ok()
{
Box::pin(FlattenedResponse::from(rx))
} else {
Box::pin(future::err(RequestError::ChannelClosed))
}
}
fn get_byte_codes(&self, request: GetByteCodesMessage) -> Self::Output {
self.get_byte_codes_with_priority(request, Priority::Normal)
}
fn get_byte_codes_with_priority(
&self,
request: GetByteCodesMessage,
priority: Priority,
) -> Self::Output {
let (response, rx) = oneshot::channel();
if self
.request_tx
.send(DownloadRequest::GetByteCodes { request, response, priority })
.is_ok()
{
Box::pin(FlattenedResponse::from(rx))
} else {
Box::pin(future::err(RequestError::ChannelClosed))
}
}
fn get_snap_block_access_lists_with_priority(
&self,
request: GetBlockAccessListsMessage,
priority: Priority,
) -> Self::Output {
let (response, rx) = oneshot::channel();
if self
.request_tx
.send(DownloadRequest::GetSnapBlockAccessLists { request, response, priority })
.is_ok()
{
Box::pin(FlattenedResponse::from(rx))
} else {
Box::pin(future::err(RequestError::ChannelClosed))
}
}
}
impl<N: NetworkPrimitives> BlockAccessListsClient for FetchClient<N> {
type Output =
std::pin::Pin<Box<dyn Future<Output = PeerRequestResult<BlockAccessLists>> + Send + Sync>>;

View File

@@ -11,6 +11,10 @@ use reth_eth_wire::{
BlockAccessLists, Capabilities, EthNetworkPrimitives, EthVersion, GetBlockAccessLists,
GetBlockBodies, GetBlockHeaders, GetReceipts, NetworkPrimitives,
};
use reth_eth_wire_types::snap::{
GetAccountRangeMessage, GetBlockAccessListsMessage, GetByteCodesMessage,
GetStorageRangesMessage, SnapProtocolMessage, SnapVersion,
};
use reth_network_api::test_utils::PeersHandle;
use reth_network_p2p::{
block_access_lists::client::BalRequirement,
@@ -18,6 +22,7 @@ use reth_network_p2p::{
headers::client::HeadersRequest,
priority::Priority,
receipts::client::ReceiptsResponse,
snap::client::SnapResponse,
};
use reth_network_peers::PeerId;
use reth_network_types::ReputationChangeKind;
@@ -37,6 +42,7 @@ type InflightHeadersRequest<H> = Request<HeadersRequest, PeerRequestResult<Vec<H
type InflightBodiesRequest<B> = Request<(), PeerRequestResult<Vec<B>>>;
type InflightReceiptsRequest<R> = Request<(), PeerRequestResult<ReceiptsResponse<R>>>;
type InflightBlockAccessListsRequest = Request<(), PeerRequestResult<BlockAccessLists>>;
type InflightSnapRequest = Request<(), PeerRequestResult<SnapResponse>>;
/// Manages data fetching operations.
///
@@ -54,6 +60,8 @@ pub struct StateFetcher<N: NetworkPrimitives = EthNetworkPrimitives> {
inflight_bals_requests: HashMap<PeerId, InflightBlockAccessListsRequest>,
/// Currently active `GetReceipts` requests
inflight_receipts_requests: HashMap<PeerId, InflightReceiptsRequest<N::Receipt>>,
/// Currently active snap protocol requests
inflight_snap_requests: HashMap<PeerId, InflightSnapRequest>,
/// The list of _available_ peers for requests.
peers: HashMap<PeerId, Peer>,
/// The handle to the peers manager
@@ -78,6 +86,7 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
inflight_bodies_requests: Default::default(),
inflight_bals_requests: Default::default(),
inflight_receipts_requests: Default::default(),
inflight_snap_requests: Default::default(),
peers: Default::default(),
peers_handle,
num_active_peers,
@@ -131,6 +140,9 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
if let Some(req) = self.inflight_receipts_requests.remove(peer) {
let _ = req.response.send(Err(RequestError::ConnectionDropped));
}
if let Some(req) = self.inflight_snap_requests.remove(peer) {
let _ = req.response.send(Err(RequestError::ConnectionDropped));
}
}
/// Updates the block information for the peer.
@@ -324,6 +336,26 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
self.inflight_receipts_requests.insert(peer_id, inflight);
BlockRequest::GetReceipts(GetReceipts(request))
}
DownloadRequest::GetAccountRange { request, response, .. } => {
let inflight = Request { request: (), response };
self.inflight_snap_requests.insert(peer_id, inflight);
BlockRequest::Snap(SnapProtocolMessage::GetAccountRange(request))
}
DownloadRequest::GetStorageRanges { request, response, .. } => {
let inflight = Request { request: (), response };
self.inflight_snap_requests.insert(peer_id, inflight);
BlockRequest::Snap(SnapProtocolMessage::GetStorageRanges(request))
}
DownloadRequest::GetByteCodes { request, response, .. } => {
let inflight = Request { request: (), response };
self.inflight_snap_requests.insert(peer_id, inflight);
BlockRequest::Snap(SnapProtocolMessage::GetByteCodes(request))
}
DownloadRequest::GetSnapBlockAccessLists { request, response, .. } => {
let inflight = Request { request: (), response };
self.inflight_snap_requests.insert(peer_id, inflight);
BlockRequest::Snap(SnapProtocolMessage::GetBlockAccessLists(request))
}
}
}
@@ -454,6 +486,27 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
None
}
/// Called on a snap protocol response from a peer
pub(crate) fn on_snap_response(
&mut self,
peer_id: PeerId,
res: RequestResult<SnapResponse>,
) -> Option<BlockResponseOutcome> {
let is_likely_bad_response = res.is_err();
if let Some(resp) = self.inflight_snap_requests.remove(&peer_id) {
let _ = resp.response.send(res.map(|r| (peer_id, r).into()));
}
if let Some(peer) = self.peers.get_mut(&peer_id) {
peer.last_response_likely_bad = is_likely_bad_response;
if peer.state.on_request_finished() && !is_likely_bad_response {
return self.followup_request(peer_id)
}
}
None
}
/// Returns a new [`FetchClient`] that can send requests to this type.
pub(crate) fn client(&self) -> FetchClient<N> {
FetchClient {
@@ -519,6 +572,7 @@ impl Peer {
fn satisfies(&self, requirement: &BestPeerRequirements) -> bool {
match requirement {
BestPeerRequirements::EthVersion(ver) => self.capabilities.supports_eth_at_least(ver),
BestPeerRequirements::SnapVersion(ver) => self.capabilities.supports_snap_version(*ver),
BestPeerRequirements::None |
BestPeerRequirements::FullBlock |
BestPeerRequirements::FullBlockRange(_) => true,
@@ -575,7 +629,9 @@ impl Peer {
BestPeerRequirements::FullBlock => self.has_full_history() && !other.has_full_history(),
// Version-based filtering happens in `next_best_peer`, so by the time we get here
// both peers already satisfy the version requirement.
BestPeerRequirements::None | BestPeerRequirements::EthVersion(_) => false,
BestPeerRequirements::None |
BestPeerRequirements::EthVersion(_) |
BestPeerRequirements::SnapVersion(_) => false,
}
}
}
@@ -593,6 +649,8 @@ enum PeerState {
GetBlockAccessLists,
/// Peer is handling a `GetReceipts` request.
GetReceipts,
/// Peer is handling a snap protocol request.
GetSnap,
/// Peer session is about to close
Closing,
}
@@ -659,6 +717,30 @@ pub(crate) enum DownloadRequest<N: NetworkPrimitives> {
response: oneshot::Sender<PeerRequestResult<ReceiptsResponse<N::Receipt>>>,
priority: Priority,
},
/// Request an account range via snap protocol
GetAccountRange {
request: GetAccountRangeMessage,
response: oneshot::Sender<PeerRequestResult<SnapResponse>>,
priority: Priority,
},
/// Request storage ranges via snap protocol
GetStorageRanges {
request: GetStorageRangesMessage,
response: oneshot::Sender<PeerRequestResult<SnapResponse>>,
priority: Priority,
},
/// Request bytecodes via snap protocol
GetByteCodes {
request: GetByteCodesMessage,
response: oneshot::Sender<PeerRequestResult<SnapResponse>>,
priority: Priority,
},
/// Request block access lists via snap/2 protocol
GetSnapBlockAccessLists {
request: GetBlockAccessListsMessage,
response: oneshot::Sender<PeerRequestResult<SnapResponse>>,
priority: Priority,
},
}
// === impl DownloadRequest ===
@@ -671,6 +753,10 @@ impl<N: NetworkPrimitives> DownloadRequest<N> {
Self::GetBlockBodies { .. } => PeerState::GetBlockBodies,
Self::GetBlockAccessLists { .. } => PeerState::GetBlockAccessLists,
Self::GetReceipts { .. } => PeerState::GetReceipts,
Self::GetAccountRange { .. } |
Self::GetStorageRanges { .. } |
Self::GetByteCodes { .. } |
Self::GetSnapBlockAccessLists { .. } => PeerState::GetSnap,
}
}
@@ -680,7 +766,11 @@ impl<N: NetworkPrimitives> DownloadRequest<N> {
Self::GetBlockHeaders { priority, .. } |
Self::GetBlockBodies { priority, .. } |
Self::GetBlockAccessLists { priority, .. } |
Self::GetReceipts { priority, .. } => priority,
Self::GetReceipts { priority, .. } |
Self::GetAccountRange { priority, .. } |
Self::GetStorageRanges { priority, .. } |
Self::GetByteCodes { priority, .. } |
Self::GetSnapBlockAccessLists { priority, .. } => priority,
}
}
@@ -701,6 +791,10 @@ impl<N: NetworkPrimitives> DownloadRequest<N> {
Self::GetBlockBodies { response, .. } => response.send(Err(err)).ok(),
Self::GetBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
Self::GetReceipts { response, .. } => response.send(Err(err)).ok(),
Self::GetAccountRange { response, .. } => response.send(Err(err)).ok(),
Self::GetStorageRanges { response, .. } => response.send(Err(err)).ok(),
Self::GetByteCodes { response, .. } => response.send(Err(err)).ok(),
Self::GetSnapBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
};
}
@@ -708,6 +802,12 @@ impl<N: NetworkPrimitives> DownloadRequest<N> {
fn best_peer_requirements(&self) -> BestPeerRequirements {
match self {
Self::GetBlockHeaders { .. } => BestPeerRequirements::None,
Self::GetAccountRange { .. } |
Self::GetStorageRanges { .. } |
Self::GetByteCodes { .. } |
Self::GetSnapBlockAccessLists { .. } => {
BestPeerRequirements::SnapVersion(SnapVersion::V2)
}
Self::GetBlockAccessLists { .. } => BestPeerRequirements::EthVersion(EthVersion::Eth71),
Self::GetBlockBodies { range_hint, .. } => {
if let Some(range) = range_hint {
@@ -753,6 +853,8 @@ enum BestPeerRequirements {
FullBlock,
/// Peer must support at least this eth protocol version.
EthVersion(EthVersion),
/// Peer must advertise this snap protocol version.
SnapVersion(SnapVersion),
}
#[cfg(test)]

View File

@@ -131,6 +131,7 @@ pub mod import;
pub mod message;
pub mod peers;
pub mod protocol;
pub mod snap_requests;
pub mod transactions;
mod budget;

View File

@@ -34,6 +34,7 @@ use crate::{
protocol::IntoRlpxSubProtocol,
required_block_filter::RequiredBlockFilter,
session::SessionManager,
snap_requests::IncomingSnapRequest,
state::NetworkState,
swarm::{Swarm, SwarmEvent},
transactions::NetworkTransactionEvent,
@@ -133,6 +134,9 @@ pub struct NetworkManager<N: NetworkPrimitives = EthNetworkPrimitives> {
/// requests. This channel size is set at
/// [`ETH_REQUEST_CHANNEL_CAPACITY`](crate::builder::ETH_REQUEST_CHANNEL_CAPACITY)
to_eth_request_handler: Option<mpsc::Sender<IncomingEthRequest<N>>>,
/// Sender half to send events to the
/// [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler) task, if configured.
to_snap_request_handler: Option<mpsc::Sender<IncomingSnapRequest>>,
/// Tracks the number of active session (connected peers).
///
/// This is updated via internal events and shared via `Arc` with the [`NetworkHandle`]
@@ -201,6 +205,30 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
self.to_eth_request_handler = Some(tx);
}
/// Sets the dedicated channel for events intended for the
/// [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler).
pub fn with_snap_request_handler(mut self, tx: mpsc::Sender<IncomingSnapRequest>) -> Self {
self.set_snap_request_handler(tx);
self
}
/// Sets the dedicated channel for events intended for the
/// [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler).
pub fn set_snap_request_handler(&mut self, tx: mpsc::Sender<IncomingSnapRequest>) {
self.to_snap_request_handler = Some(tx);
}
/// Creates a [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler) and wires it to
/// the network manager, returning the handler to be spawned by the caller.
pub fn snap_request_handler<S: reth_network_p2p::snap::server::SnapStateProvider>(
&mut self,
snap_provider: S,
) -> crate::snap_requests::SnapRequestHandler<S> {
let (tx, rx) = mpsc::channel(256);
self.set_snap_request_handler(tx);
crate::snap_requests::SnapRequestHandler::new(snap_provider, rx)
}
/// Adds an additional protocol handler to the `RLPx` sub-protocol list.
pub fn add_rlpx_sub_protocol(&mut self, protocol: impl IntoRlpxSubProtocol) {
self.swarm.add_rlpx_sub_protocol(protocol)
@@ -364,6 +392,7 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
event_sender,
to_transactions_manager: None,
to_eth_request_handler: None,
to_snap_request_handler: None,
num_active_peers,
metrics: Default::default(),
disconnect_metrics: Default::default(),
@@ -514,6 +543,18 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
}
}
/// Sends an event to the [`SnapRequestHandler`](crate::snap_requests::SnapRequestHandler) if
/// configured.
fn delegate_snap_request(&self, event: IncomingSnapRequest) {
if let Some(ref reqs) = self.to_snap_request_handler {
let _ = reqs.try_send(event).map_err(|e| {
if let TrySendError::Full(_) = e {
debug!(target:"net", "SnapRequestHandler channel is full!");
}
});
}
}
/// Handle an incoming request from the peer
fn on_eth_request(&self, peer_id: PeerId, req: PeerRequest<N>) {
match req {
@@ -566,6 +607,33 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
response,
})
}
PeerRequest::GetAccountRange { request, response } => {
self.delegate_snap_request(IncomingSnapRequest::GetAccountRange {
peer_id,
request,
response,
})
}
PeerRequest::GetStorageRanges { request, response } => {
self.delegate_snap_request(IncomingSnapRequest::GetStorageRanges {
peer_id,
request,
response,
})
}
PeerRequest::GetByteCodes { request, response } => {
self.delegate_snap_request(IncomingSnapRequest::GetByteCodes {
peer_id,
request,
response,
})
}
PeerRequest::GetSnapBlockAccessLists { request, response } => self
.delegate_snap_request(IncomingSnapRequest::GetBlockAccessLists {
peer_id,
request,
response,
}),
PeerRequest::GetPooledTransactions { request, response } => {
self.notify_tx_manager(NetworkTransactionEvent::GetPooledTransactions {
peer_id,

View File

@@ -13,9 +13,12 @@ use reth_eth_wire::{
NetworkPrimitives, NewBlock, NewBlockHashes, NewBlockPayload, NewPooledTransactionHashes,
NodeData, PooledTransactions, Receipts, SharedTransactions, Transactions,
};
use reth_eth_wire_types::RawCapabilityMessage;
use reth_eth_wire_types::{snap::SnapProtocolMessage, RawCapabilityMessage};
use reth_network_api::PeerRequest;
use reth_network_p2p::error::{RequestError, RequestResult};
use reth_network_p2p::{
error::{RequestError, RequestResult},
snap::client::SnapResponse,
};
use reth_primitives_traits::Block;
use std::{
sync::Arc,
@@ -128,6 +131,9 @@ pub enum BlockRequest {
///
/// The response should be sent through the channel.
GetReceipts(GetReceipts),
/// A snap protocol request.
Snap(SnapProtocolMessage),
}
/// Corresponding variant for [`PeerRequest`].
@@ -177,6 +183,11 @@ pub enum PeerResponse<N: NetworkPrimitives = EthNetworkPrimitives> {
/// The receiver channel for the response to a block access lists request.
response: oneshot::Receiver<RequestResult<BlockAccessLists>>,
},
/// Represents a response to a snap protocol request.
Snap {
/// The receiver channel for the snap response.
response: oneshot::Receiver<RequestResult<SnapResponse>>,
},
}
// === impl PeerResponse ===
@@ -220,6 +231,10 @@ impl<N: NetworkPrimitives> PeerResponse<N> {
Ok(res) => PeerResponseResult::BlockAccessLists(res),
Err(err) => PeerResponseResult::BlockAccessLists(Err(err.into())),
},
Self::Snap { response } => match ready!(response.poll_unpin(cx)) {
Ok(res) => PeerResponseResult::Snap(res),
Err(err) => PeerResponseResult::Snap(Err(err.into())),
},
};
Poll::Ready(res)
}
@@ -244,6 +259,8 @@ pub enum PeerResponseResult<N: NetworkPrimitives = EthNetworkPrimitives> {
Receipts70(RequestResult<Receipts70<N::Receipt>>),
/// Represents a result containing block access lists or an error.
BlockAccessLists(RequestResult<BlockAccessLists>),
/// Represents a result containing a snap protocol response or an error.
Snap(RequestResult<SnapResponse>),
}
// === impl PeerResponseResult ===
@@ -295,6 +312,10 @@ impl<N: NetworkPrimitives> PeerResponseResult<N> {
}
Err(err) => Err(err),
},
Self::Snap(_) => {
// Snap responses are not sent via EthMessage; they use the snap sub-protocol.
Err(RequestError::BadResponse)
}
}
}
@@ -309,6 +330,7 @@ impl<N: NetworkPrimitives> PeerResponseResult<N> {
Self::Receipts69(res) => res.as_ref().err(),
Self::Receipts70(res) => res.as_ref().err(),
Self::BlockAccessLists(res) => res.as_ref().err(),
Self::Snap(res) => res.as_ref().err(),
}
}

View File

@@ -371,14 +371,128 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
OnIncomingMessageOutcome::Ok
}
EthMessage::Other(bytes) => self.try_emit_broadcast(PeerMessage::Other(bytes)).into(),
EthMessage::Other(raw_msg) => {
// Check if this is a snap protocol response by trying to decode it.
let eth_msg_count = reth_eth_wire::EthMessageID::message_count(self.conn.version());
let raw_id = raw_msg.id as u8;
if raw_id >= eth_msg_count {
let snap_id = raw_id - eth_msg_count;
if let Some(snap_version) = self.negotiated_snap_version() &&
let Ok(snap_msg) =
reth_eth_wire_types::snap::SnapProtocolMessage::decode_with_version(
snap_version,
snap_id,
&mut raw_msg.payload.as_ref(),
)
{
return self.on_incoming_snap_response(snap_msg);
}
}
self.try_emit_broadcast(PeerMessage::Other(raw_msg)).into()
}
}
}
/// Handle an incoming snap protocol response by matching it to an inflight request.
fn on_incoming_snap_response(
&mut self,
snap_msg: reth_eth_wire_types::snap::SnapProtocolMessage,
) -> OnIncomingMessageOutcome<N> {
use reth_eth_wire_types::snap::SnapProtocolMessage;
use reth_network_p2p::snap::client::SnapResponse;
let (request_id, snap_response) = match snap_msg {
SnapProtocolMessage::AccountRange(msg) => {
(msg.request_id, SnapResponse::AccountRange(msg))
}
SnapProtocolMessage::StorageRanges(msg) => {
(msg.request_id, SnapResponse::StorageRanges(msg))
}
SnapProtocolMessage::ByteCodes(msg) => (msg.request_id, SnapResponse::ByteCodes(msg)),
SnapProtocolMessage::BlockAccessLists(msg) => {
(msg.request_id, SnapResponse::BlockAccessLists(msg))
}
// Incoming snap *requests* from the remote peer are handled separately
_ => {
let peer_req = self.create_snap_incoming_request(snap_msg);
return self.try_emit_request(PeerMessage::EthRequest(peer_req)).into();
}
};
if let Some(req) = self.inflight_requests.remove(&request_id) {
match req.request {
RequestState::Waiting(
PeerRequest::GetAccountRange { response, .. } |
PeerRequest::GetStorageRanges { response, .. } |
PeerRequest::GetByteCodes { response, .. } |
PeerRequest::GetSnapBlockAccessLists { response, .. },
) => {
trace!(peer_id=?self.remote_peer_id, ?request_id, "received snap response from peer");
let _ = response.send(Ok(snap_response));
self.update_request_timeout(req.timestamp, Instant::now());
}
RequestState::Waiting(request) => {
request.send_bad_response();
}
RequestState::TimedOut => {
self.update_request_timeout(req.timestamp, Instant::now());
}
}
} else {
trace!(peer_id=?self.remote_peer_id, ?request_id, "received snap response to unknown request");
self.on_bad_message();
}
OnIncomingMessageOutcome::Ok
}
/// Creates a `PeerRequest` for an incoming snap request from the remote peer.
///
/// This converts snap protocol request messages into `PeerRequest` variants so they can be
/// dispatched to the snap request handler.
fn create_snap_incoming_request(
&mut self,
snap_msg: reth_eth_wire_types::snap::SnapProtocolMessage,
) -> PeerRequest<N> {
use reth_eth_wire_types::snap::SnapProtocolMessage;
let (tx, response) = oneshot::channel();
let request_id = match &snap_msg {
SnapProtocolMessage::GetAccountRange(msg) => msg.request_id,
SnapProtocolMessage::GetStorageRanges(msg) => msg.request_id,
SnapProtocolMessage::GetByteCodes(msg) => msg.request_id,
SnapProtocolMessage::GetBlockAccessLists(msg) => msg.request_id,
_ => unreachable!("only snap/2 request variants reach here"),
};
let received = ReceivedRequest {
request_id,
rx: PeerResponse::Snap { response },
received: Instant::now(),
};
self.received_requests_from_remote.push(received);
match snap_msg {
SnapProtocolMessage::GetAccountRange(req) => {
PeerRequest::GetAccountRange { request: req, response: tx }
}
SnapProtocolMessage::GetStorageRanges(req) => {
PeerRequest::GetStorageRanges { request: req, response: tx }
}
SnapProtocolMessage::GetByteCodes(req) => {
PeerRequest::GetByteCodes { request: req, response: tx }
}
SnapProtocolMessage::GetBlockAccessLists(req) => {
PeerRequest::GetSnapBlockAccessLists { request: req, response: tx }
}
_ => unreachable!("only snap/2 request variants reach here"),
}
}
/// Handle an internal peer request that will be sent to the remote.
fn on_internal_peer_request(&mut self, request: PeerRequest<N>, deadline: Instant) {
let version = self.conn.version();
if !Self::is_request_supported_for_version(&request, version) {
if !Self::is_request_supported_for_peer(&request, version, &self.remote_capabilities) {
debug!(
target: "net",
?request,
@@ -392,9 +506,26 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
let request_id = self.next_id();
trace!(?request, peer_id=?self.remote_peer_id, ?request_id, "sending request to peer");
let msg = request.create_request_message(request_id).map_versioned(version);
self.queued_outgoing.push_back(msg.into());
if request.is_snap_request() {
// Snap requests are encoded as snap protocol messages and sent as raw
// capability messages through the multiplexed connection.
let snap_msg = request.create_snap_request_message(request_id);
let encoded = snap_msg.encode();
// Adjust the message ID for multiplexing: add the eth message count offset
let eth_msg_count = reth_eth_wire::EthMessageID::message_count(version);
let mut adjusted = Vec::with_capacity(encoded.len());
adjusted.push(encoded[0] + eth_msg_count);
adjusted.extend_from_slice(&encoded[1..]);
self.queued_outgoing.push_back(OutgoingMessage::Raw(RawCapabilityMessage::new(
adjusted[0] as usize,
adjusted[1..].to_vec().into(),
)));
} else {
let msg = request.create_request_message(request_id).map_versioned(version);
self.queued_outgoing.push_back(msg.into());
}
let req = InflightRequest {
request: RequestState::Waiting(request),
timestamp: Instant::now(),
@@ -408,6 +539,25 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
request.is_supported_by_eth_version(version)
}
#[inline]
fn is_request_supported_for_peer(
request: &PeerRequest<N>,
version: EthVersion,
capabilities: &Capabilities,
) -> bool {
Self::is_request_supported_for_version(request, version) &&
request
.required_snap_version()
.is_none_or(|snap_version| capabilities.supports_snap_version(snap_version))
}
#[inline]
fn negotiated_snap_version(&self) -> Option<reth_eth_wire_types::snap::SnapVersion> {
self.remote_capabilities
.supports_snap_v2()
.then_some(reth_eth_wire_types::snap::SnapVersion::V2)
}
/// Handle a message received from the internal network
fn on_internal_peer_message(&mut self, msg: PeerMessage<N>) {
match msg {
@@ -452,6 +602,35 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
///
/// This will queue the response to be sent to the peer
fn handle_outgoing_response(&mut self, id: u64, resp: PeerResponseResult<N>) {
if let PeerResponseResult::Snap(res) = resp {
// Snap responses need to be sent as raw capability messages
if let Ok(snap_resp) = res {
let snap_msg = match snap_resp {
reth_network_p2p::snap::client::SnapResponse::AccountRange(msg) => {
reth_eth_wire_types::snap::SnapProtocolMessage::AccountRange(msg)
}
reth_network_p2p::snap::client::SnapResponse::StorageRanges(msg) => {
reth_eth_wire_types::snap::SnapProtocolMessage::StorageRanges(msg)
}
reth_network_p2p::snap::client::SnapResponse::ByteCodes(msg) => {
reth_eth_wire_types::snap::SnapProtocolMessage::ByteCodes(msg)
}
reth_network_p2p::snap::client::SnapResponse::BlockAccessLists(msg) => {
reth_eth_wire_types::snap::SnapProtocolMessage::BlockAccessLists(msg)
}
};
let encoded = snap_msg.encode();
let eth_msg_count = reth_eth_wire::EthMessageID::message_count(self.conn.version());
let mut adjusted = Vec::with_capacity(encoded.len());
adjusted.push(encoded[0] + eth_msg_count);
adjusted.extend_from_slice(&encoded[1..]);
self.queued_outgoing.push_back(OutgoingMessage::Raw(RawCapabilityMessage::new(
adjusted[0] as usize,
adjusted[1..].to_vec().into(),
)));
}
return;
}
match resp.try_into_message(id) {
Ok(msg) => {
self.queued_outgoing.push_back(msg.into());

View File

@@ -1149,10 +1149,10 @@ async fn authenticate_stream<N: NetworkPrimitives>(
// Before trying status handshake, set up the version to negotiated shared version
status.set_eth_version(eth_version);
let (conn, their_status) = if p2p_stream.shared_capabilities().len() == 1 {
// if the shared caps are 1, we know both support the eth version
// if the hello handshake was successful we can try status handshake
let (conn, their_status) = if extra_handlers.is_empty() {
// Without dedicated extra handlers, keep the session on the eth stream. The underlying
// p2p stream still preserves relative capability IDs, so snap messages can be handled by
// the session as raw eth `Other` messages.
// perform the eth protocol handshake
match handshake
.handshake(&mut p2p_stream, status, fork_filter.clone(), HANDSHAKE_TIMEOUT)

View File

@@ -0,0 +1,191 @@
//! Snap protocol request handling for serving state data to peers.
use crate::{budget::DEFAULT_BUDGET_TRY_DRAIN_DOWNLOADERS, metered_poll_nested_stream_with_budget};
use futures::StreamExt;
use reth_eth_wire_types::snap::{
AccountRangeMessage, BlockAccessListsMessage, ByteCodesMessage, GetAccountRangeMessage,
GetBlockAccessListsMessage, GetByteCodesMessage, GetStorageRangesMessage, StorageRangesMessage,
};
use reth_network_p2p::{
error::RequestResult,
snap::{client::SnapResponse, server::SnapStateProvider},
};
use reth_network_peers::PeerId;
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use tokio::sync::{mpsc::Receiver, oneshot};
use tokio_stream::wrappers::ReceiverStream;
/// Manages incoming snap protocol requests from peers.
///
/// This should be spawned as a background task, similar to
/// [`EthRequestHandler`](crate::eth_requests::EthRequestHandler).
#[derive(Debug)]
#[must_use = "Handler does nothing unless polled."]
pub struct SnapRequestHandler<S> {
snap_provider: S,
incoming_requests: ReceiverStream<IncomingSnapRequest>,
}
impl<S> SnapRequestHandler<S> {
/// Creates a new handler with the given provider and receiver channel.
pub fn new(snap_provider: S, incoming: Receiver<IncomingSnapRequest>) -> Self {
Self { snap_provider, incoming_requests: ReceiverStream::new(incoming) }
}
}
impl<S: SnapStateProvider> SnapRequestHandler<S> {
fn on_account_range_request(
&self,
_peer_id: PeerId,
request: GetAccountRangeMessage,
response: oneshot::Sender<RequestResult<SnapResponse>>,
) {
let (accounts, proof) = self.snap_provider.account_range(
request.root_hash,
request.starting_hash,
request.limit_hash,
request.response_bytes,
);
let _ = response.send(Ok(SnapResponse::AccountRange(AccountRangeMessage {
request_id: request.request_id,
accounts,
proof,
})));
}
fn on_storage_ranges_request(
&self,
_peer_id: PeerId,
request: GetStorageRangesMessage,
response: oneshot::Sender<RequestResult<SnapResponse>>,
) {
let (slots, proof) = self.snap_provider.storage_ranges(
request.root_hash,
request.account_hashes,
request.starting_hash,
request.limit_hash,
request.response_bytes,
);
let _ = response.send(Ok(SnapResponse::StorageRanges(StorageRangesMessage {
request_id: request.request_id,
slots,
proof,
})));
}
fn on_byte_codes_request(
&self,
_peer_id: PeerId,
request: GetByteCodesMessage,
response: oneshot::Sender<RequestResult<SnapResponse>>,
) {
let codes = self.snap_provider.bytecodes(request.hashes, request.response_bytes);
let _ = response.send(Ok(SnapResponse::ByteCodes(ByteCodesMessage {
request_id: request.request_id,
codes,
})));
}
fn on_block_access_lists_request(
&self,
_peer_id: PeerId,
request: GetBlockAccessListsMessage,
response: oneshot::Sender<RequestResult<SnapResponse>>,
) {
let block_access_lists =
self.snap_provider.block_access_lists(request.block_hashes, request.response_bytes);
let _ = response.send(Ok(SnapResponse::BlockAccessLists(BlockAccessListsMessage {
request_id: request.request_id,
block_access_lists,
})));
}
}
impl<S: SnapStateProvider + Unpin> Future for SnapRequestHandler<S> {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
let mut acc = Duration::ZERO;
let maybe_more_incoming_requests = metered_poll_nested_stream_with_budget!(
acc,
"net::snap",
"Incoming snap requests stream",
DEFAULT_BUDGET_TRY_DRAIN_DOWNLOADERS,
this.incoming_requests.poll_next_unpin(cx),
|incoming| {
match incoming {
IncomingSnapRequest::GetAccountRange { peer_id, request, response } => {
this.on_account_range_request(peer_id, request, response)
}
IncomingSnapRequest::GetStorageRanges { peer_id, request, response } => {
this.on_storage_ranges_request(peer_id, request, response)
}
IncomingSnapRequest::GetByteCodes { peer_id, request, response } => {
this.on_byte_codes_request(peer_id, request, response)
}
IncomingSnapRequest::GetBlockAccessLists { peer_id, request, response } => {
this.on_block_access_lists_request(peer_id, request, response)
}
}
},
);
if maybe_more_incoming_requests {
cx.waker().wake_by_ref();
}
Poll::Pending
}
}
/// Incoming snap protocol requests delegated by the [`NetworkManager`](crate::NetworkManager).
#[derive(Debug)]
pub enum IncomingSnapRequest {
/// Request for an account range.
GetAccountRange {
/// The ID of the peer requesting account range.
peer_id: PeerId,
/// The account range request.
request: GetAccountRangeMessage,
/// The channel sender for the response.
response: oneshot::Sender<RequestResult<SnapResponse>>,
},
/// Request for storage ranges.
GetStorageRanges {
/// The ID of the peer requesting storage ranges.
peer_id: PeerId,
/// The storage ranges request.
request: GetStorageRangesMessage,
/// The channel sender for the response.
response: oneshot::Sender<RequestResult<SnapResponse>>,
},
/// Request for bytecodes.
GetByteCodes {
/// The ID of the peer requesting bytecodes.
peer_id: PeerId,
/// The bytecodes request.
request: GetByteCodesMessage,
/// The channel sender for the response.
response: oneshot::Sender<RequestResult<SnapResponse>>,
},
/// Request for block access lists.
GetBlockAccessLists {
/// The ID of the peer requesting BALs.
peer_id: PeerId,
/// The snap/2 BAL request.
request: GetBlockAccessListsMessage,
/// The channel sender for the response.
response: oneshot::Sender<RequestResult<SnapResponse>>,
},
}

View File

@@ -16,6 +16,7 @@ use reth_eth_wire::{
BlockHashNumber, Capabilities, DisconnectReason, EthNetworkPrimitives, GetReceipts70,
NetworkPrimitives, NewBlockHashes, NewBlockPayload, UnifiedStatus,
};
use reth_eth_wire_types::snap::SnapProtocolMessage;
use reth_ethereum_forks::ForkId;
use reth_network_api::{DiscoveredEvent, DiscoveryEvent, PeerRequest, PeerRequestSender};
use reth_network_p2p::receipts::client::ReceiptsResponse;
@@ -435,6 +436,26 @@ impl<N: NetworkPrimitives> NetworkState<N> {
(request, response)
}
}
BlockRequest::Snap(snap_msg) => {
let (response, rx) = oneshot::channel();
let request = match snap_msg {
SnapProtocolMessage::GetAccountRange(req) => {
PeerRequest::GetAccountRange { request: req, response }
}
SnapProtocolMessage::GetStorageRanges(req) => {
PeerRequest::GetStorageRanges { request: req, response }
}
SnapProtocolMessage::GetByteCodes(req) => {
PeerRequest::GetByteCodes { request: req, response }
}
SnapProtocolMessage::GetBlockAccessLists(req) => {
PeerRequest::GetSnapBlockAccessLists { request: req, response }
}
_ => unreachable!("only request variants are used"),
};
let response = PeerResponse::Snap { response: rx };
(request, response)
}
};
let _ = peer.request_tx.to_session_tx.try_send(request);
peer.pending_response = Some(response);
@@ -490,6 +511,7 @@ impl<N: NetworkPrimitives> NetworkState<N> {
PeerResponseResult::BlockAccessLists(res) => {
self.state_fetcher.on_block_access_lists_response(peer, res)
}
PeerResponseResult::Snap(res) => self.state_fetcher.on_snap_response(peer, res),
_ => None,
};

View File

@@ -7,7 +7,7 @@ use rand::Rng;
use reth_eth_wire::{BlockAccessLists, EthVersion, GetBlockAccessLists, HeadersDirection};
use reth_ethereum_primitives::Block;
use reth_network::{
eth_requests::SOFT_RESPONSE_LIMIT,
eth_requests::{MAX_BLOCK_ACCESS_LISTS_SERVE, SOFT_RESPONSE_LIMIT},
test_utils::{NetworkEventStream, PeerConfig, Testnet, TestnetHandle},
BlockDownloaderProvider, NetworkEventListenerProvider,
};
@@ -607,6 +607,26 @@ async fn test_eth71_get_block_access_lists_empty_request() {
assert_eq!(response, BlockAccessLists(Vec::new()));
}
// Ensures BAL responses are capped at MAX_BLOCK_ACCESS_LISTS_SERVE entries.
#[tokio::test(flavor = "multi_thread")]
async fn test_eth71_get_block_access_lists_caps_count() {
reth_tracing::init_test_tracing();
let (net, bal_store) = spawn_eth71_bal_testnet().await;
// Request more hashes than the count cap.
let request_count = MAX_BLOCK_ACCESS_LISTS_SERVE + 100;
let hashes: Vec<B256> = (0..request_count).map(|_| B256::random()).collect();
// Insert one BAL so the store isn't entirely empty (not strictly needed,
// but keeps the test path closer to real usage).
let bal = Bytes::from_static(&[0xc1, 0x01]);
bal_store.insert(hashes[0], 1, bal).unwrap();
let response = request_block_access_lists(&net, hashes).await;
assert_eq!(response.0.len(), MAX_BLOCK_ACCESS_LISTS_SERVE);
}
// Ensures the fetch client can request BALs through an eth/71 peer.
#[tokio::test(flavor = "multi_thread")]
async fn test_eth71_fetch_client_get_block_access_lists() {

View File

@@ -1,8 +1,8 @@
use crate::{download::DownloadClient, error::PeerRequestResult, priority::Priority};
use futures::Future;
use reth_eth_wire_types::snap::{
AccountRangeMessage, ByteCodesMessage, GetAccountRangeMessage, GetByteCodesMessage,
GetStorageRangesMessage, GetTrieNodesMessage, StorageRangesMessage, TrieNodesMessage,
AccountRangeMessage, BlockAccessListsMessage, ByteCodesMessage, GetAccountRangeMessage,
GetBlockAccessListsMessage, GetByteCodesMessage, GetStorageRangesMessage, StorageRangesMessage,
};
/// Response types for snap sync requests
@@ -14,8 +14,8 @@ pub enum SnapResponse {
StorageRanges(StorageRangesMessage),
/// Response containing bytecode data
ByteCodes(ByteCodesMessage),
/// Response containing trie node data
TrieNodes(TrieNodesMessage),
/// Response containing snap/2 block access list data
BlockAccessLists(BlockAccessListsMessage),
}
/// The snap sync downloader client
@@ -62,15 +62,16 @@ pub trait SnapClient: DownloadClient {
priority: Priority,
) -> Self::Output;
/// Sends the trie nodes request to the p2p network and returns the trie nodes
/// response received from a peer.
fn get_trie_nodes(&self, request: GetTrieNodesMessage) -> Self::Output;
/// Sends the snap/2 block access lists request to the p2p network and returns the response
/// received from a peer.
fn get_snap_block_access_lists(&self, request: GetBlockAccessListsMessage) -> Self::Output {
self.get_snap_block_access_lists_with_priority(request, Priority::Normal)
}
/// Sends the trie nodes request to the p2p network with priority set and returns
/// the trie nodes response received from a peer.
fn get_trie_nodes_with_priority(
/// Sends the snap/2 block access lists request to the p2p network with priority set.
fn get_snap_block_access_lists_with_priority(
&self,
request: GetTrieNodesMessage,
request: GetBlockAccessListsMessage,
priority: Priority,
) -> Self::Output;
}

View File

@@ -1,2 +1,4 @@
/// SNAP related traits.
pub mod client;
/// Server-side snap request handling traits.
pub mod server;

View File

@@ -0,0 +1,40 @@
use alloy_primitives::{Bytes, B256};
use reth_eth_wire_types::{
snap::{AccountData, StorageData},
BlockAccessLists,
};
/// Provides state and BAL data for serving snap protocol requests.
pub trait SnapStateProvider: Send + Sync + 'static {
/// Iterates accounts in hash-sorted order from `starting_hash` up to, but not including,
/// `limit_hash`. Returns at most `response_bytes` worth of data.
///
/// The second return value contains boundary proof nodes when available.
fn account_range(
&self,
root_hash: B256,
starting_hash: B256,
limit_hash: B256,
response_bytes: u64,
) -> (Vec<AccountData>, Vec<Bytes>);
/// Iterates storage slots for the given account hashes.
///
/// The second return value contains boundary proof nodes when available.
fn storage_ranges(
&self,
root_hash: B256,
account_hashes: Vec<B256>,
starting_hash: B256,
limit_hash: B256,
response_bytes: u64,
) -> (Vec<Vec<StorageData>>, Vec<Bytes>);
/// Returns bytecodes for the given code hashes.
fn bytecodes(&self, hashes: Vec<B256>, response_bytes: u64) -> Vec<Bytes>;
/// Returns snap/2 BAL response entries for the given block hashes.
///
/// Missing BALs must be encoded as the RLP empty string (`0x80`) for snap/2.
fn block_access_lists(&self, block_hashes: Vec<B256>, response_bytes: u64) -> BlockAccessLists;
}

View File

@@ -24,6 +24,7 @@ reth-db-common.workspace = true
reth-downloaders.workspace = true
reth-engine-local.workspace = true
reth-engine-primitives.workspace = true
reth-engine-snap.workspace = true
reth-engine-tree.workspace = true
reth-engine-util.workspace = true
reth-evm.workspace = true
@@ -49,6 +50,7 @@ reth-rpc-eth-types.workspace = true
reth-rpc-layer.workspace = true
reth-stages.workspace = true
reth-static-file.workspace = true
reth-storage-api = { workspace = true, features = ["db-api"] }
reth-tasks = { workspace = true, features = ["rayon"] }
reth-tokio-util.workspace = true
reth-tracing.workspace = true
@@ -120,6 +122,7 @@ test-utils = [
"reth-node-ethereum/test-utils",
"reth-primitives-traits/test-utils",
"reth-tasks/test-utils",
"reth-engine-snap/test-utils",
]
trie-debug = [
"reth-engine-tree/trie-debug",

View File

@@ -913,13 +913,19 @@ impl<Node: FullNodeTypes> BuilderContext<Node> {
PropPolicy: TransactionPropagationPolicy<N>,
AnnPolicy: AnnouncementFilteringPolicy<N>,
{
let (handle, network, txpool, eth) = builder
let mut net_builder = builder
.transactions_with_policies(pool, tx_config, propagation_policy, announcement_policy)
.request_handler(self.provider().clone())
.split_with_handle();
.request_handler(self.provider().clone());
let snap = net_builder.snap_request_handler(
reth_engine_snap::serve::ProviderSnapState::new(self.provider().clone()),
);
let (handle, network, txpool, eth) = net_builder.split_with_handle();
self.executor.spawn_critical_blocking_task("p2p txpool", txpool);
self.executor.spawn_critical_blocking_task("p2p eth request handler", eth);
self.executor.spawn_critical_blocking_task("p2p snap request handler", snap);
let default_peers_path = self.config().datadir().known_peers();
let known_peers_file = self.config().network.persistent_peers_file(default_peers_path);

View File

@@ -34,6 +34,8 @@ use reth_provider::{
providers::{BlockchainProvider, NodeTypesForProvider},
BlockNumReader, StorageSettingsCache,
};
use reth_rpc_engine_api::BalCache;
use reth_storage_api::BalStoreHandle;
use reth_tasks::TaskExecutor;
use reth_tokio_util::EventSender;
use reth_tracing::tracing::{debug, error, info};
@@ -75,6 +77,10 @@ impl EngineNodeLauncher {
>,
>,
CB: NodeComponentsBuilder<T>,
<CB::Components as NodeComponents<T>>::Network: BlockDownloaderProvider<
Client: reth_network_p2p::snap::client::SnapClient
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient,
>,
AO: RethRpcAddOns<NodeAdapter<T, CB::Components>>
+ EngineValidatorAddOn<NodeAdapter<T, CB::Components>>,
{
@@ -121,7 +127,9 @@ impl EngineNodeLauncher {
// passing FullNodeTypes as type parameter here so that we can build
// later the components.
.with_blockchain_db::<T, _>(move |provider_factory| {
Ok(BlockchainProvider::new(provider_factory)?)
let mut provider = BlockchainProvider::new(provider_factory)?;
provider.set_bal_store(BalStoreHandle::new(BalCache::new()));
Ok(provider)
})?
.with_components(components_builder, on_component_initialized).await?;
@@ -326,9 +334,15 @@ impl EngineNodeLauncher {
network_handle.update_sync_state(SyncState::Idle);
}
}
ChainEvent::BackfillSyncStarted => {
ChainEvent::BackfillSyncStarted |
ChainEvent::SnapSyncStarted => {
network_handle.update_sync_state(SyncState::Syncing);
}
ChainEvent::SnapSyncFinished => {
if startup_sync_state_idle {
network_handle.update_sync_state(SyncState::Idle);
}
}
ChainEvent::FatalError => {
error!(target: "reth::cli", "Fatal error in consensus engine");
res = Err(eyre::eyre!("Fatal error in consensus engine"));
@@ -436,6 +450,10 @@ where
>,
>,
CB: NodeComponentsBuilder<T> + 'static,
<CB::Components as NodeComponents<T>>::Network: BlockDownloaderProvider<
Client: reth_network_p2p::snap::client::SnapClient
+ reth_network_p2p::block_access_lists::client::BlockAccessListsClient,
>,
AO: RethRpcAddOns<NodeAdapter<T, CB::Components>>
+ EngineValidatorAddOn<NodeAdapter<T, CB::Components>>
+ 'static,

View File

@@ -44,6 +44,7 @@ use reth_rpc_builder::{
};
use reth_rpc_engine_api::{capabilities::EngineCapabilities, EngineApi};
use reth_rpc_eth_types::{cache::cache_new_blocks_task, EthConfig, EthStateCache};
use reth_storage_api::BalProvider;
use reth_tokio_util::EventSender;
use reth_tracing::tracing::{debug, info};
use std::{
@@ -1518,7 +1519,9 @@ where
commit: version_metadata().vergen_git_sha.to_string(),
};
Ok(EngineApi::new(
let bal_store = ctx.node.provider().bal_store().clone();
Ok(EngineApi::with_bal_store(
ctx.node.provider().clone(),
ctx.config.chain.clone(),
ctx.beacon_engine_handle.clone(),
@@ -1530,6 +1533,7 @@ where
engine_validator,
ctx.config.engine.accept_execution_requests_hash,
ctx.node.network().clone(),
bal_store,
))
}
}

View File

@@ -289,7 +289,7 @@ impl Default for DefaultEngineValues {
share_execution_cache_with_payload_builder: false,
share_sparse_trie_with_payload_builder: false,
suppress_persistence_during_build: false,
bal_parallel_execution_disabled: false,
bal_parallel_execution_disabled: true,
bal_parallel_state_root_disabled: false,
}
}
@@ -511,8 +511,8 @@ pub struct EngineArgs {
)]
pub suppress_persistence_during_build: bool,
/// Disable BAL (Block Access List, EIP-7928) based parallel execution. When set, falls back
/// to transaction-based prewarming even when a BAL is available.
/// Disable BAL (Block Access List, EIP-7928) based parallel execution. Defaults to disabled,
/// falling back to transaction-based prewarming even when a BAL is available.
#[arg(long = "engine.disable-bal-parallel-execution", default_value_t = DefaultEngineValues::get_global().bal_parallel_execution_disabled)]
pub bal_parallel_execution_disabled: bool,

View File

@@ -141,6 +141,13 @@ where
Self::Right(r) => r.requests(),
}
}
fn block_access_list(&self) -> Option<&alloy_primitives::Bytes> {
match self {
Self::Left(l) => l.block_access_list(),
Self::Right(r) => r.block_access_list(),
}
}
}
impl<L, R> PayloadBuilder for PayloadBuilderStack<L, R>

View File

@@ -54,6 +54,17 @@ pub trait PayloadTypes: Send + Sync + Unpin + core::fmt::Debug + Clone + 'static
<<Self::BuiltPayload as BuiltPayload>::Primitives as NodePrimitives>::Block,
>,
) -> Self::ExecutionData;
/// Converts a built payload into the execution payload format.
///
/// Unlike [`block_to_payload`](Self::block_to_payload), this has access to the full built
/// payload including sidecar data such as the block access list (BAL).
///
/// The default implementation delegates to [`block_to_payload`](Self::block_to_payload),
/// discarding any extra data beyond the block itself.
fn built_payload_to_execution_data(payload: &Self::BuiltPayload) -> Self::ExecutionData {
Self::block_to_payload(payload.block().clone())
}
}
/// Validates the timestamp depending on the version called:

View File

@@ -65,6 +65,9 @@ pub trait ExecutionPayload:
///
/// Returns `None` for pre-Amsterdam blocks.
fn slot_number(&self) -> Option<u64>;
/// Returns this block's state root.
fn state_root(&self) -> B256;
}
impl ExecutionPayload for ExecutionData {
@@ -111,6 +114,10 @@ impl ExecutionPayload for ExecutionData {
fn slot_number(&self) -> Option<u64> {
self.payload.slot_number()
}
fn state_root(&self) -> B256 {
self.payload.as_v1().state_root
}
}
/// A unified type for handling both execution payloads and payload attributes.

View File

@@ -93,6 +93,13 @@ pub trait BuiltPayload: Send + Sync + fmt::Debug {
/// These are requests generated by the execution layer that need to be
/// processed by the consensus layer (e.g., validator deposits, withdrawals).
fn requests(&self) -> Option<Requests>;
/// Returns the RLP-encoded block access list (BAL) for Amsterdam+ blocks.
///
/// Returns `None` for pre-Amsterdam blocks or when no BAL is available.
fn block_access_list(&self) -> Option<&alloy_primitives::Bytes> {
None
}
}
/// Basic attributes required to initiate payload construction.

View File

@@ -16,6 +16,7 @@ workspace = true
reth-chainspec.workspace = true
reth-rpc-api.workspace = true
reth-storage-api.workspace = true
reth-storage-errors.workspace = true
reth-payload-builder.workspace = true
reth-payload-builder-primitives.workspace = true
reth-payload-primitives.workspace = true
@@ -42,6 +43,7 @@ metrics.workspace = true
async-trait.workspace = true
jsonrpsee-core.workspace = true
jsonrpsee-types.workspace = true
parking_lot.workspace = true
serde.workspace = true
thiserror.workspace = true
tracing.workspace = true

View File

@@ -0,0 +1,296 @@
//! Block Access List (BAL) cache for EIP-7928.
//!
//! This module provides an in-memory cache for storing Block Access Lists received via
//! the Engine API. BALs are stored for valid payloads and can be retrieved via
//! `engine_getBALsByHashV1` and `engine_getBALsByRangeV1`.
//!
//! According to EIP-7928, the EL MUST retain BALs for at least the duration of the
//! weak subjectivity period (~3533 epochs) to support synchronization with re-execution.
//! This initial implementation uses a simple in-memory cache with configurable capacity.
use alloy_primitives::{BlockHash, BlockNumber, Bytes};
use parking_lot::RwLock;
use reth_metrics::{
metrics::{Counter, Gauge},
Metrics,
};
use reth_storage_api::BalStore;
use reth_storage_errors::provider::ProviderResult;
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
/// Default capacity for the BAL cache.
///
/// This is a conservative default - production deployments should configure based on
/// weak subjectivity period requirements (~3533 epochs ≈ 113,000 blocks).
const DEFAULT_BAL_CACHE_CAPACITY: u32 = 1024;
/// In-memory cache for Block Access Lists (BALs).
///
/// Provides O(1) lookups by block hash and O(log n) range queries by block number.
/// Evicts the oldest (lowest) block numbers when capacity is exceeded.
///
/// This type is cheaply cloneable as it wraps an `Arc` internally.
#[derive(Debug, Clone)]
pub struct BalCache {
inner: Arc<BalCacheInner>,
}
#[derive(Debug)]
struct BalCacheInner {
/// Maximum number of entries to store.
capacity: u32,
/// Mapping from block hash to BAL bytes.
entries: RwLock<HashMap<BlockHash, Bytes>>,
/// Index mapping block number to block hash for range queries.
/// Uses `BTreeMap` for efficient range iteration and eviction of oldest blocks.
block_index: RwLock<BTreeMap<BlockNumber, BlockHash>>,
/// Cache metrics.
metrics: BalCacheMetrics,
}
impl BalCache {
/// Creates a new BAL cache with the default capacity.
pub fn new() -> Self {
Self::with_capacity(DEFAULT_BAL_CACHE_CAPACITY)
}
/// Creates a new BAL cache with the specified capacity.
pub fn with_capacity(capacity: u32) -> Self {
Self {
inner: Arc::new(BalCacheInner {
capacity,
entries: RwLock::new(HashMap::new()),
block_index: RwLock::new(BTreeMap::new()),
metrics: BalCacheMetrics::default(),
}),
}
}
/// Inserts a BAL into the cache.
///
/// If a different hash already exists for this block number (reorg), the old entry
/// is removed first. If the cache is at capacity, the oldest block number is evicted.
pub fn insert(&self, block_hash: BlockHash, block_number: BlockNumber, bal: Bytes) {
let mut entries = self.inner.entries.write();
let mut block_index = self.inner.block_index.write();
// If this block number already has a different hash, remove the old entry
if let Some(old_hash) = block_index.get(&block_number) &&
*old_hash != block_hash
{
entries.remove(old_hash);
}
// Evict oldest block if at capacity and this is a new entry
if !entries.contains_key(&block_hash) &&
entries.len() as u32 >= self.inner.capacity &&
let Some((&oldest_num, &oldest_hash)) = block_index.first_key_value()
{
entries.remove(&oldest_hash);
block_index.remove(&oldest_num);
}
entries.insert(block_hash, bal);
block_index.insert(block_number, block_hash);
self.inner.metrics.inserts.increment(1);
self.inner.metrics.count.set(entries.len() as f64);
}
/// Retrieves BALs for the given block hashes.
///
/// Returns a vector with the same length as `block_hashes`, where each element
/// is `Some(bal)` if found or `None` if not in cache.
pub fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> Vec<Option<Bytes>> {
let entries = self.inner.entries.read();
block_hashes
.iter()
.map(|hash| {
let result = entries.get(hash).cloned();
if result.is_some() {
self.inner.metrics.hits.increment(1);
} else {
self.inner.metrics.misses.increment(1);
}
result
})
.collect()
}
/// Retrieves BALs for a range of blocks starting at `start` for `count` blocks.
///
/// Returns a vector of contiguous BALs in block number order, stopping at the first
/// missing block. This ensures the caller knows the returned BALs correspond to
/// blocks `[start, start + len)`.
pub fn get_by_range(&self, start: BlockNumber, count: u64) -> Vec<Bytes> {
let entries = self.inner.entries.read();
let block_index = self.inner.block_index.read();
let mut result = Vec::new();
for block_num in start..start.saturating_add(count) {
let Some(hash) = block_index.get(&block_num) else {
break;
};
let Some(bal) = entries.get(hash) else {
break;
};
result.push(bal.clone());
}
result
}
/// Returns the number of entries in the cache.
#[cfg(test)]
fn len(&self) -> usize {
self.inner.entries.read().len()
}
}
impl Default for BalCache {
fn default() -> Self {
Self::new()
}
}
impl BalStore for BalCache {
fn insert(
&self,
block_hash: BlockHash,
block_number: BlockNumber,
bal: Bytes,
) -> ProviderResult<()> {
BalCache::insert(self, block_hash, block_number, bal);
Ok(())
}
fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> ProviderResult<Vec<Option<Bytes>>> {
Ok(BalCache::get_by_hashes(self, block_hashes))
}
fn get_by_range(&self, start: BlockNumber, count: u64) -> ProviderResult<Vec<Bytes>> {
Ok(BalCache::get_by_range(self, start, count))
}
}
/// Metrics for the BAL cache.
#[derive(Metrics)]
#[metrics(scope = "engine.bal_cache")]
struct BalCacheMetrics {
/// The total number of BALs in the cache.
count: Gauge,
/// The number of cache inserts.
inserts: Counter,
/// The number of cache hits.
hits: Counter,
/// The number of cache misses.
misses: Counter,
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::B256;
#[test]
fn test_insert_and_get_by_hash() {
let cache = BalCache::with_capacity(10);
let hash1 = B256::random();
let hash2 = B256::random();
let bal1 = Bytes::from_static(b"bal1");
let bal2 = Bytes::from_static(b"bal2");
cache.insert(hash1, 1, bal1.clone());
cache.insert(hash2, 2, bal2.clone());
let results = cache.get_by_hashes(&[hash1, hash2, B256::random()]);
assert_eq!(results.len(), 3);
assert_eq!(results[0], Some(bal1));
assert_eq!(results[1], Some(bal2));
assert_eq!(results[2], None);
}
#[test]
fn test_get_by_range() {
let cache = BalCache::with_capacity(10);
for i in 1..=5 {
let hash = B256::random();
let bal = Bytes::from(format!("bal{i}").into_bytes());
cache.insert(hash, i, bal);
}
let results = cache.get_by_range(2, 3);
assert_eq!(results.len(), 3);
}
#[test]
fn test_get_by_range_stops_at_gap() {
let cache = BalCache::with_capacity(10);
// Insert blocks 1, 2, 4, 5 (missing block 3)
for i in [1, 2, 4, 5] {
let hash = B256::random();
let bal = Bytes::from(format!("bal{i}").into_bytes());
cache.insert(hash, i, bal);
}
// Requesting range starting at 1 should stop at the gap (block 3)
let results = cache.get_by_range(1, 5);
assert_eq!(results.len(), 2); // Only blocks 1 and 2
// Requesting range starting at 4 should return 4 and 5
let results = cache.get_by_range(4, 3);
assert_eq!(results.len(), 2);
}
#[test]
fn test_eviction_oldest_first() {
let cache = BalCache::with_capacity(3);
// Insert blocks 10, 20, 30
for i in [10, 20, 30] {
let hash = B256::random();
cache.insert(hash, i, Bytes::from_static(b"bal"));
}
assert_eq!(cache.len(), 3);
// Insert block 40, should evict block 10 (oldest/lowest)
let hash40 = B256::random();
cache.insert(hash40, 40, Bytes::from_static(b"bal40"));
assert_eq!(cache.len(), 3);
// Block 10 should be gone, block 20 should still be there
let results = cache.get_by_range(10, 1);
assert_eq!(results.len(), 0);
let results = cache.get_by_range(20, 1);
assert_eq!(results.len(), 1);
}
#[test]
fn test_reorg_replaces_hash() {
let cache = BalCache::with_capacity(10);
let hash1 = B256::random();
let hash2 = B256::random();
let bal1 = Bytes::from_static(b"bal1");
let bal2 = Bytes::from_static(b"bal2");
// Insert block 100 with hash1
cache.insert(hash1, 100, bal1.clone());
assert_eq!(cache.get_by_hashes(&[hash1])[0], Some(bal1));
// Reorg: insert block 100 with hash2
cache.insert(hash2, 100, bal2.clone());
// hash1 should be gone, hash2 should be there
assert_eq!(cache.get_by_hashes(&[hash1])[0], None);
assert_eq!(cache.get_by_hashes(&[hash2])[0], Some(bal2));
assert_eq!(cache.len(), 1);
}
}

View File

@@ -1,13 +1,15 @@
use crate::{
capabilities::EngineCapabilities, metrics::EngineApiMetrics, EngineApiError, EngineApiResult,
bal_cache::BalCache, capabilities::EngineCapabilities, metrics::EngineApiMetrics,
EngineApiError, EngineApiResult,
};
use alloy_eips::{
eip1898::BlockHashOrNumber,
eip4844::{BlobAndProofV1, BlobAndProofV2},
eip4895::Withdrawals,
eip7685::RequestsOrHash,
BlockNumHash,
};
use alloy_primitives::{BlockHash, BlockNumber, B256, U64};
use alloy_primitives::{BlockHash, BlockNumber, Bytes, B256, U64};
use alloy_rpc_types_engine::{
CancunPayloadFields, ClientVersionV1, ExecutionData, ExecutionPayloadBodiesV1,
ExecutionPayloadBodiesV2, ExecutionPayloadBodyV1, ExecutionPayloadBodyV2,
@@ -22,12 +24,12 @@ use reth_engine_primitives::{ConsensusEngineHandle, EngineApiValidator, EngineTy
use reth_network_api::NetworkInfo;
use reth_payload_builder::PayloadStore;
use reth_payload_primitives::{
validate_payload_timestamp, EngineApiMessageVersion, MessageValidationKind,
PayloadOrAttributes, PayloadTypes,
validate_payload_timestamp, BuiltPayload, EngineApiMessageVersion, ExecutionPayload,
MessageValidationKind, PayloadOrAttributes, PayloadTypes,
};
use reth_primitives_traits::{Block, BlockBody};
use reth_rpc_api::{EngineApiServer, IntoEngineApiRpcModule};
use reth_storage_api::{BlockReader, HeaderProvider, StateProviderFactory};
use reth_storage_api::{BalStoreHandle, BlockReader, HeaderProvider, StateProviderFactory};
use reth_tasks::Runtime;
use reth_transaction_pool::TransactionPool;
use std::{
@@ -97,6 +99,73 @@ where
validator: Validator,
accept_execution_requests_hash: bool,
network: impl NetworkInfo + 'static,
) -> Self {
Self::with_bal_store(
provider,
chain_spec,
beacon_consensus,
payload_store,
tx_pool,
task_spawner,
client,
capabilities,
validator,
accept_execution_requests_hash,
network,
BalStoreHandle::new(BalCache::new()),
)
}
/// Create new instance of [`EngineApi`] with a custom BAL cache.
#[expect(clippy::too_many_arguments)]
pub fn with_bal_cache(
provider: Provider,
chain_spec: Arc<ChainSpec>,
beacon_consensus: ConsensusEngineHandle<PayloadT>,
payload_store: PayloadStore<PayloadT>,
tx_pool: Pool,
task_spawner: Runtime,
client: ClientVersionV1,
capabilities: EngineCapabilities,
validator: Validator,
accept_execution_requests_hash: bool,
network: impl NetworkInfo + 'static,
bal_cache: BalCache,
) -> Self {
Self::with_bal_store(
provider,
chain_spec,
beacon_consensus,
payload_store,
tx_pool,
task_spawner,
client,
capabilities,
validator,
accept_execution_requests_hash,
network,
BalStoreHandle::new(bal_cache),
)
}
/// Create new instance of [`EngineApi`] with an explicit BAL store handle.
///
/// Use this when sharing a [`BalStoreHandle`] between the Engine API and the
/// P2P `EthRequestHandler` so both read/write the same underlying store.
#[expect(clippy::too_many_arguments)]
pub fn with_bal_store(
provider: Provider,
chain_spec: Arc<ChainSpec>,
beacon_consensus: ConsensusEngineHandle<PayloadT>,
payload_store: PayloadStore<PayloadT>,
tx_pool: Pool,
task_spawner: Runtime,
client: ClientVersionV1,
capabilities: EngineCapabilities,
validator: Validator,
accept_execution_requests_hash: bool,
network: impl NetworkInfo + 'static,
bal_store: BalStoreHandle,
) -> Self {
let is_syncing = Arc::new(move || network.is_syncing());
let inner = Arc::new(EngineApiInner {
@@ -112,10 +181,25 @@ where
validator,
accept_execution_requests_hash,
is_syncing,
bal_store,
});
Self { inner }
}
/// Returns a reference to the BAL store handle.
pub fn bal_store(&self) -> &BalStoreHandle {
&self.inner.bal_store
}
/// Caches the BAL if the status is valid.
fn maybe_cache_bal(&self, num_hash: BlockNumHash, bal: Option<Bytes>, status: &PayloadStatus) {
if status.is_valid() &&
let Some(bal) = bal
{
let _ = self.inner.bal_store.insert(num_hash.hash, num_hash.number, bal);
}
}
/// Fetches the client version.
pub fn get_client_version_v1(
&self,
@@ -150,7 +234,11 @@ where
.validator
.validate_version_specific_fields(EngineApiMessageVersion::V1, payload_or_attrs)?;
Ok(self.inner.beacon_consensus.new_payload(payload).await?)
let num_hash = payload.num_hash();
let bal = payload.block_access_list().cloned();
let status = self.inner.beacon_consensus.new_payload(payload).await?;
self.maybe_cache_bal(num_hash, bal, &status);
Ok(status)
}
/// Metered version of `new_payload_v1`.
@@ -178,7 +266,12 @@ where
self.inner
.validator
.validate_version_specific_fields(EngineApiMessageVersion::V2, payload_or_attrs)?;
Ok(self.inner.beacon_consensus.new_payload(payload).await?)
let num_hash = payload.num_hash();
let bal = payload.block_access_list().cloned();
let status = self.inner.beacon_consensus.new_payload(payload).await?;
self.maybe_cache_bal(num_hash, bal, &status);
Ok(status)
}
/// Metered version of `new_payload_v2`.
@@ -207,7 +300,11 @@ where
.validator
.validate_version_specific_fields(EngineApiMessageVersion::V3, payload_or_attrs)?;
Ok(self.inner.beacon_consensus.new_payload(payload).await?)
let num_hash = payload.num_hash();
let bal = payload.block_access_list().cloned();
let status = self.inner.beacon_consensus.new_payload(payload).await?;
self.maybe_cache_bal(num_hash, bal, &status);
Ok(status)
}
/// Metrics version of `new_payload_v3`
@@ -237,7 +334,11 @@ where
.validator
.validate_version_specific_fields(EngineApiMessageVersion::V4, payload_or_attrs)?;
Ok(self.inner.beacon_consensus.new_payload(payload).await?)
let num_hash = payload.num_hash();
let bal = payload.block_access_list().cloned();
let status = self.inner.beacon_consensus.new_payload(payload).await?;
self.maybe_cache_bal(num_hash, bal, &status);
Ok(status)
}
/// Metrics version of `new_payload_v4`
@@ -407,12 +508,23 @@ where
&self,
payload_id: PayloadId,
) -> EngineApiResult<EngineT::BuiltPayload> {
self.inner
let payload = self
.inner
.payload_store
.resolve(payload_id)
.await
.ok_or(EngineApiError::UnknownPayload)?
.map_err(|_| EngineApiError::UnknownPayload)
.map_err(|_| EngineApiError::UnknownPayload)?;
// Cache the BAL eagerly — the built payload carries the BAL bytes but
// they may be lost during the ExecutionData V3/V4 conversion when the
// header doesn't yet have block_access_list_hash set.
if let Some(bal) = payload.block_access_list() {
let num_hash = payload.block().num_hash();
let _ = self.inner.bal_store.insert(num_hash.hash, num_hash.number, bal.clone());
}
Ok(payload)
}
/// Helper function for validating the payload timestamp and retrieving & converting the payload
@@ -1009,6 +1121,22 @@ where
res
}
/// Retrieves BALs for the given block hashes from the store.
///
/// Returns the RLP-encoded BALs for blocks found in the store.
/// Missing blocks are returned as empty bytes.
pub fn get_bals_by_hash(&self, block_hashes: Vec<BlockHash>) -> Vec<alloy_primitives::Bytes> {
let results = self.inner.bal_store.get_by_hashes(&block_hashes).unwrap_or_default();
results.into_iter().map(|opt| opt.unwrap_or_default()).collect()
}
/// Retrieves BALs for a range of blocks from the store.
///
/// Returns the RLP-encoded BALs for blocks in the range `[start, start + count)`.
pub fn get_bals_by_range(&self, start: u64, count: u64) -> Vec<alloy_primitives::Bytes> {
self.inner.bal_store.get_by_range(start, count).unwrap_or_default()
}
}
// This is the concrete ethereum engine API implementation.
@@ -1433,6 +1561,8 @@ struct EngineApiInner<Provider, PayloadT: PayloadTypes, Pool, Validator, ChainSp
accept_execution_requests_hash: bool,
/// Returns `true` if the node is currently syncing.
is_syncing: Arc<dyn Fn() -> bool + Send + Sync>,
/// Store for Block Access Lists (BALs) per EIP-7928.
bal_store: BalStoreHandle,
}
#[cfg(test)]

View File

@@ -15,6 +15,10 @@ mod engine_api;
/// Reth-specific engine API extensions.
mod reth_engine_api;
/// Block Access List (BAL) cache for EIP-7928.
mod bal_cache;
pub use bal_cache::BalCache;
/// Engine API capabilities.
pub mod capabilities;
pub use capabilities::EngineCapabilities;

View File

@@ -9,9 +9,10 @@ use alloy_primitives::{B256, U256};
use alloy_rpc_types_eth::BlockNumberOrTag;
use futures::Future;
use reth_chain_state::{BlockState, ComputedTrieData, ExecutedBlock};
use reth_chainspec::{ChainSpecProvider, EthChainSpec};
use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
use reth_errors::{BlockExecutionError, BlockValidationError, ProviderError, RethError};
use reth_evm::{
block::TxResult,
execute::{BlockBuilder, BlockBuilderOutcome, BlockExecutionOutput},
ConfigureEvm, Evm, EvmEnvFor, NextBlockEnvAttributes,
};
@@ -30,7 +31,7 @@ use reth_transaction_pool::{
error::InvalidPoolTransactionError, BestTransactions, BestTransactionsAttributes,
PoolTransaction, TransactionPool,
};
use revm::context_interface::Block;
use revm::context_interface::{Block, Cfg as _};
use std::{
sync::Arc,
time::{Duration, Instant},
@@ -263,6 +264,10 @@ pub trait LoadPendingBlock:
builder.apply_pre_execution_changes().map_err(Self::Error::from_eth_err)?;
let block_gas_limit: u64 = builder.evm().block().gas_limit();
let is_amsterdam = self
.provider()
.chain_spec()
.is_amsterdam_active_at_timestamp(builder.evm().block().timestamp().saturating_to());
let basefee = builder.evm().block().basefee();
let blob_gasprice = builder.evm().block().blob_gasprice().map(|p| p as u64);
@@ -271,8 +276,11 @@ pub trait LoadPendingBlock:
.chain_spec()
.blob_params_at_timestamp(parent.timestamp())
.unwrap_or_else(BlobParams::cancun);
let mut cumulative_gas_used = 0;
let mut cumulative_tx_gas_used = 0;
let mut block_regular_gas_used = 0;
let mut block_state_gas_used = 0;
let mut sum_blob_gas_used = 0;
let tx_gas_limit_cap = builder.evm().cfg_env().tx_gas_limit_cap();
// Only include transactions if not configured as Empty
if !self.pending_block_kind().is_empty() {
@@ -287,15 +295,35 @@ pub trait LoadPendingBlock:
while let Some(pool_tx) = best_txs.next() {
// ensure we still have capacity for this transaction
if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
let exceeds_gas_limit = if is_amsterdam {
let regular_available_gas =
block_gas_limit.saturating_sub(block_regular_gas_used);
let state_available_gas = block_gas_limit.saturating_sub(block_state_gas_used);
let regular_tx_gas_limit = pool_tx.gas_limit().min(tx_gas_limit_cap);
if regular_tx_gas_limit > regular_available_gas {
Some((regular_tx_gas_limit, regular_available_gas))
} else if pool_tx.gas_limit() > state_available_gas {
Some((pool_tx.gas_limit(), state_available_gas))
} else {
None
}
} else {
let block_available_gas =
block_gas_limit.saturating_sub(cumulative_tx_gas_used);
(pool_tx.gas_limit() > block_available_gas)
.then_some((pool_tx.gas_limit(), block_available_gas))
};
if let Some((transaction_gas_limit, block_available_gas)) = exceeds_gas_limit {
// we can't fit this transaction into the block, so we need to mark it as
// invalid which also removes all dependent transaction from
// the iterator before we can continue
best_txs.mark_invalid(
&pool_tx,
&InvalidPoolTransactionError::ExceedsGasLimit(
pool_tx.gas_limit(),
block_gas_limit,
transaction_gas_limit,
block_available_gas,
),
);
continue
@@ -337,29 +365,48 @@ pub trait LoadPendingBlock:
continue
}
let gas_used = match builder.execute_transaction(tx) {
Ok(gas_used) => gas_used,
Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
error,
..
})) => {
if error.is_nonce_too_low() {
// if the nonce is too low, we can skip this transaction
} else {
// if the transaction is invalid, we can skip it and all of its
// descendants
let mut tx_regular_gas_used = 0;
let gas_output =
match builder.execute_transaction_with_result_closure(tx, |result| {
tx_regular_gas_used = result.result().result.gas().block_regular_gas_used();
}) {
Ok(gas_output) => gas_output,
Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
error,
..
})) => {
if error.is_nonce_too_low() {
// if the nonce is too low, we can skip this transaction
} else {
// if the transaction is invalid, we can skip it and all of its
// descendants
best_txs.mark_invalid(
&pool_tx,
&InvalidPoolTransactionError::Consensus(
InvalidTransactionError::TxTypeNotSupported,
),
);
}
continue
}
Err(BlockExecutionError::Validation(
BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
transaction_gas_limit,
block_available_gas,
},
)) => {
best_txs.mark_invalid(
&pool_tx,
&InvalidPoolTransactionError::Consensus(
InvalidTransactionError::TxTypeNotSupported,
&InvalidPoolTransactionError::ExceedsGasLimit(
transaction_gas_limit,
block_available_gas,
),
);
continue
}
continue
}
// this is an error that we should treat as fatal for this attempt
Err(err) => return Err(Self::Error::from_eth_err(err)),
};
// this is an error that we should treat as fatal for this attempt
Err(err) => return Err(Self::Error::from_eth_err(err)),
};
// add to the total blob gas used if the transaction successfully executed
if let Some(tx_blob_gas) = tx_blob_gas {
@@ -371,9 +418,11 @@ pub trait LoadPendingBlock:
}
}
// add gas used by the transaction to cumulative gas used, before creating the
// receipt
cumulative_gas_used += gas_used;
// Track receipt gas and the Amsterdam block-capacity counter separately.
let gas_used = gas_output.tx_gas_used();
cumulative_tx_gas_used += gas_used;
block_regular_gas_used += tx_regular_gas_used;
block_state_gas_used += gas_output.state_gas_used();
}
}

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