mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
30 Commits
bal-devnet
...
snapv2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d545d1bd5 | ||
|
|
35e901ea51 | ||
|
|
af1b99f0d7 | ||
|
|
a46ef1cd91 | ||
|
|
7708ac59e7 | ||
|
|
f7863c766b | ||
|
|
8324ee1173 | ||
|
|
00f6c75f06 | ||
|
|
2fce3e701d | ||
|
|
938313028d | ||
|
|
0790359003 | ||
|
|
089c0e2629 | ||
|
|
bc80e2a66b | ||
|
|
9af8265047 | ||
|
|
f3e30a3111 | ||
|
|
6715a093f1 | ||
|
|
d5bff1d478 | ||
|
|
20141a2ea0 | ||
|
|
57962a1b95 | ||
|
|
60468fe256 | ||
|
|
ce3a171ce0 | ||
|
|
ce7e80ad33 | ||
|
|
0c6a10d3fa | ||
|
|
d41a9a4078 | ||
|
|
b984ddd275 | ||
|
|
b9c330e1a9 | ||
|
|
cd10e6b47c | ||
|
|
7b2c458302 | ||
|
|
0722202930 | ||
|
|
8ec6e614f9 |
35
.github/scripts/hive/expected_failures.yaml
vendored
35
.github/scripts/hive/expected_failures.yaml
vendored
@@ -140,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
|
||||
@@ -241,23 +256,3 @@ eels/consume-rlp:
|
||||
- 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
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_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_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_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_from_state_test]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_from_state_test-empty-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Paris-blockchain_test_from_state_test]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_from_state_test-empty-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_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_from_state_test-initcode-with-deploy]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Cancun-blockchain_test_from_state_test]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Shanghai-blockchain_test_from_state_test-sstore-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_from_state_test-initcode-with-deploy]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_from_state_test-sstore-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_from_state_test-empty-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_collision_with_create2_revert_in_initcode[fork_Osaka-blockchain_test_from_state_test]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Prague-blockchain_test_from_state_test-sstore-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Cancun-blockchain_test_from_state_test-sstore-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Paris-blockchain_test_from_state_test-sstore-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_from_state_test-empty-initcode]-reth
|
||||
- tests/paris/eip7610_create_collision/test_revert_in_create.py::test_create2_collision_storage[fork_Osaka-blockchain_test_from_state_test-initcode-with-deploy]-reth
|
||||
|
||||
11
.github/workflows/hive.yml
vendored
11
.github/workflows/hive.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -23,9 +26,9 @@ 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-16' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -191,7 +194,7 @@ jobs:
|
||||
- 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' && (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:
|
||||
@@ -364,7 +367,7 @@ jobs:
|
||||
- prepare-hive
|
||||
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:
|
||||
|
||||
410
Cargo.lock
generated
410
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
41
Cargo.toml
41
Cargo.toml
@@ -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,14 +435,14 @@ reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
|
||||
reth-zstd-compressors = { version = "0.3.1", default-features = false }
|
||||
|
||||
# revm
|
||||
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-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-inspectors = "0.39.0"
|
||||
revm = { version = "=38.0.0", default-features = false }
|
||||
revm-bytecode = { version = "=10.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.1", default-features = false }
|
||||
revm-database-interface = { version = "=11.0.1", default-features = false }
|
||||
revm-inspectors = "=0.39.0"
|
||||
|
||||
# eth
|
||||
alloy-dyn-abi = "1.5.6"
|
||||
@@ -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.3", 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 }
|
||||
|
||||
@@ -700,3 +702,24 @@ vergen-git2 = "9.1.0"
|
||||
|
||||
# networking
|
||||
ipnet = "2.11"
|
||||
|
||||
[patch.crates-io]
|
||||
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" }
|
||||
|
||||
@@ -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,
|
||||
@@ -116,7 +116,7 @@ pub(crate) type BalIndexReader<DB> = fn(&DB) -> u64;
|
||||
/// 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,
|
||||
{
|
||||
@@ -145,6 +145,21 @@ where
|
||||
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>
|
||||
where
|
||||
DB: StateDB,
|
||||
@@ -431,12 +446,8 @@ where
|
||||
self.inner_mut().execute_transaction_without_commit(tx)
|
||||
}
|
||||
|
||||
fn commit_transaction(
|
||||
&mut self,
|
||||
output: Self::Result,
|
||||
) -> Result<GasOutput, BlockExecutionError> {
|
||||
self.maybe_apply_boundary()?;
|
||||
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
|
||||
@@ -451,7 +462,7 @@ where
|
||||
if let Some(plan) = &mut self.plan {
|
||||
plan.tx_counter += 1;
|
||||
}
|
||||
Ok(gas_used)
|
||||
gas_used
|
||||
}
|
||||
|
||||
fn finish(
|
||||
@@ -613,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
|
||||
@@ -622,10 +636,10 @@ 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.peek_plan();
|
||||
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, None, None)
|
||||
|
||||
@@ -169,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>,
|
||||
|
||||
@@ -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 {} {}",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
65
crates/engine/snap/Cargo.toml
Normal file
65
crates/engine/snap/Cargo.toml
Normal 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",
|
||||
]
|
||||
332
crates/engine/snap/src/bal.rs
Normal file
332
crates/engine/snap/src/bal.rs
Normal 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])));
|
||||
}
|
||||
}
|
||||
314
crates/engine/snap/src/controller.rs
Normal file
314
crates/engine/snap/src/controller.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
650
crates/engine/snap/src/download.rs
Normal file
650
crates/engine/snap/src/download.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
68
crates/engine/snap/src/finalize.rs
Normal file
68
crates/engine/snap/src/finalize.rs
Normal 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(())
|
||||
}
|
||||
118
crates/engine/snap/src/lib.rs
Normal file
118
crates/engine/snap/src/lib.rs
Normal 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
|
||||
/// HEAD−N, 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),
|
||||
}
|
||||
277
crates/engine/snap/src/orchestrator.rs
Normal file
277
crates/engine/snap/src/orchestrator.rs
Normal 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() })
|
||||
}
|
||||
}
|
||||
310
crates/engine/snap/src/pivot.rs
Normal file
310
crates/engine/snap/src/pivot.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
440
crates/engine/snap/src/proof.rs
Normal file
440
crates/engine/snap/src/proof.rs
Normal 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 { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
481
crates/engine/snap/src/serve.rs
Normal file
481
crates/engine/snap/src/serve.rs
Normal 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)
|
||||
}
|
||||
184
crates/engine/snap/src/storage.rs
Normal file
184
crates/engine/snap/src/storage.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1159,19 +1159,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
let account = revm_state::Account {
|
||||
info: AccountInfo {
|
||||
balance: U256::from(rng.random::<u64>()),
|
||||
nonce: rng.random::<u64>(),
|
||||
code_hash: KECCAK_EMPTY,
|
||||
code: Some(Default::default()),
|
||||
account_id: None,
|
||||
},
|
||||
original_info: Box::new(AccountInfo::default()),
|
||||
storage,
|
||||
status: AccountStatus::Touched,
|
||||
transaction_id: 0,
|
||||
let mut account = revm_state::Account::default();
|
||||
account.info = AccountInfo {
|
||||
balance: U256::from(rng.random::<u64>()),
|
||||
nonce: rng.random::<u64>(),
|
||||
code_hash: KECCAK_EMPTY,
|
||||
code: Some(Default::default()),
|
||||
account_id: None,
|
||||
};
|
||||
account.storage = storage;
|
||||
account.status = AccountStatus::Touched;
|
||||
|
||||
state_update.insert(address, account);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -568,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),
|
||||
@@ -650,6 +652,7 @@ where
|
||||
transaction_root,
|
||||
receipt_root_bloom,
|
||||
hashed_state,
|
||||
built_bal
|
||||
),
|
||||
block
|
||||
);
|
||||
@@ -906,6 +909,7 @@ where
|
||||
BlockExecutionOutput<N::Receipt>,
|
||||
Vec<Address>,
|
||||
tokio::sync::oneshot::Receiver<(B256, alloy_primitives::Bloom)>,
|
||||
Option<BlockAccessList>,
|
||||
),
|
||||
InsertBlockErrorKind,
|
||||
>
|
||||
@@ -913,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()
|
||||
});
|
||||
|
||||
@@ -980,6 +998,7 @@ where
|
||||
handle.iter_transactions(),
|
||||
&receipt_tx,
|
||||
&executed_tx_index,
|
||||
has_bal,
|
||||
)?;
|
||||
drop(receipt_tx);
|
||||
|
||||
@@ -994,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();
|
||||
@@ -1001,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.
|
||||
@@ -1013,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);
|
||||
@@ -1035,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();
|
||||
@@ -1079,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);
|
||||
|
||||
@@ -1362,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>,
|
||||
@@ -1388,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())
|
||||
|
||||
@@ -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"),
|
||||
})
|
||||
}
|
||||
|
||||
174
crates/engine/tree/src/tree/snap.rs
Normal file
174
crates/engine/tree/src/tree/snap.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::{
|
||||
@@ -283,12 +283,12 @@ where
|
||||
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
|
||||
@@ -307,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
|
||||
@@ -331,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 {
|
||||
@@ -358,7 +358,7 @@ where
|
||||
Ok(sidecar) => Some(sidecar),
|
||||
Err(error) => {
|
||||
best_txs.mark_invalid(&pool_tx, &InvalidPoolTransactionError::Eip4844(error));
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -388,7 +388,7 @@ where
|
||||
),
|
||||
);
|
||||
}
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
// The executor is the source of truth for block gas availability. Keep this
|
||||
// non-fatal in case local builder accounting diverges from executor rules.
|
||||
@@ -406,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)),
|
||||
@@ -443,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.
|
||||
@@ -485,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);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
use crate::{ConfigureEvm, Database, OnStateHook, TxEnvFor};
|
||||
use alloc::{boxed::Box, sync::Arc, vec::Vec};
|
||||
use alloy_consensus::{BlockHeader, Header};
|
||||
use alloy_eips::eip2718::WithEncoded;
|
||||
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},
|
||||
@@ -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.
|
||||
@@ -453,7 +470,11 @@ 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(
|
||||
@@ -466,6 +487,8 @@ where
|
||||
self.executor.execute_transaction_with_commit_condition((tx_env, &tx), f)?
|
||||
{
|
||||
self.transactions.push(tx);
|
||||
// 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]
|
||||
|
||||
@@ -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,
|
||||
> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -829,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;
|
||||
|
||||
@@ -874,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,
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
@@ -327,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)),
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -339,6 +339,8 @@ where
|
||||
.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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
|
||||
191
crates/net/network/src/snap_requests.rs
Normal file
191
crates/net/network/src/snap_requests.rs
Normal 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>>,
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
/// SNAP related traits.
|
||||
pub mod client;
|
||||
/// Server-side snap request handling traits.
|
||||
pub mod server;
|
||||
|
||||
40
crates/net/p2p/src/snap/server.rs
Normal file
40
crates/net/p2p/src/snap/server.rs
Normal 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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
296
crates/rpc/rpc-engine-api/src/bal_cache.rs
Normal file
296
crates/rpc/rpc-engine-api/src/bal_cache.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -202,7 +202,7 @@ where
|
||||
// update the cached reads
|
||||
self.update_cached_reads(parent_header_hash, request_cache).await;
|
||||
|
||||
self.consensus.validate_block_post_execution(&block, &output, None)?;
|
||||
self.consensus.validate_block_post_execution(&block, &output, None, None)?;
|
||||
|
||||
self.ensure_payment(&block, &output, &message)?;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use reth_primitives_traits::constants::BEACON_CONSENSUS_REORG_UNWIND_DEPTH;
|
||||
use reth_provider::{
|
||||
providers::ProviderNodeTypes, BlockHashReader, BlockNumReader, ChainStateBlockReader,
|
||||
ChainStateBlockWriter, DBProvider, DatabaseProviderFactory, ProviderFactory,
|
||||
PruneCheckpointReader, StageCheckpointReader, StageCheckpointWriter,
|
||||
PruneCheckpointReader, StageCheckpointReader, StageCheckpointWriter, StorageSettingsCache,
|
||||
};
|
||||
use reth_prune::PrunerBuilder;
|
||||
use reth_static_file::StaticFileProducer;
|
||||
@@ -272,6 +272,12 @@ impl<N: ProviderNodeTypes> Pipeline<N> {
|
||||
/// CAUTION: This method locks the static file producer Mutex, hence can block the thread if the
|
||||
/// lock is occupied.
|
||||
pub fn move_to_static_files(&self) -> RethResult<()> {
|
||||
// In storage v2, receipts are written inline to static files during execution,
|
||||
// so the legacy copy-from-DB path is not needed.
|
||||
if self.provider_factory.cached_storage_settings().is_v2() {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// Copies data from database to static files
|
||||
let lowest_static_file_height =
|
||||
self.static_file_producer.lock().copy_to_static_files()?.min_block_num();
|
||||
|
||||
@@ -4,7 +4,7 @@ use alloy_primitives::BlockNumber;
|
||||
use num_traits::Zero;
|
||||
use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
|
||||
use reth_config::config::ExecutionConfig;
|
||||
use reth_consensus::FullConsensus;
|
||||
use reth_consensus::{validate_block_access_list_gas, FullConsensus};
|
||||
use reth_db::{static_file::HeaderMask, tables};
|
||||
use reth_evm::{execute::Executor, metrics::ExecutorMetrics, ConfigureEvm};
|
||||
use reth_execution_types::Chain;
|
||||
@@ -357,7 +357,19 @@ where
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Err(err) = self.consensus.validate_block_post_execution(&block, &result, None) {
|
||||
let bal = executor.take_bal().unwrap_or_default();
|
||||
if block.header().block_access_list_hash().is_some() &&
|
||||
let Err(err) = validate_block_access_list_gas(Some(&bal), block.gas_limit())
|
||||
{
|
||||
return Err(StageError::Block {
|
||||
block: Box::new(block.block_with_parent()),
|
||||
error: BlockErrorKind::Validation(err),
|
||||
})
|
||||
}
|
||||
|
||||
if let Err(err) =
|
||||
self.consensus.validate_block_post_execution(&block, &result, None, Some(bal))
|
||||
{
|
||||
return Err(StageError::Block {
|
||||
block: Box::new(block.block_with_parent()),
|
||||
error: BlockErrorKind::Validation(err),
|
||||
|
||||
@@ -115,6 +115,16 @@ impl<N: ProviderNodeTypes> BlockchainProvider<N> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying [`ProviderFactory`].
|
||||
pub fn database(&self) -> &ProviderFactory<N> {
|
||||
&self.database
|
||||
}
|
||||
|
||||
/// Sets the BAL store for this provider.
|
||||
pub fn set_bal_store(&mut self, bal_store: BalStoreHandle) {
|
||||
self.bal_store = bal_store;
|
||||
}
|
||||
|
||||
/// Gets a clone of `canonical_in_memory_state`.
|
||||
pub fn canonical_in_memory_state(&self) -> CanonicalInMemoryState<N::Primitives> {
|
||||
self.canonical_in_memory_state.clone()
|
||||
|
||||
@@ -900,18 +900,6 @@ impl<TX: DbTx + DbTxMut + 'static, N: NodeTypesForProvider> DatabaseProvider<TX,
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes bytecodes to MDBX.
|
||||
fn write_bytecodes(
|
||||
&self,
|
||||
bytecodes: impl IntoIterator<Item = (B256, Bytecode)>,
|
||||
) -> ProviderResult<()> {
|
||||
let mut bytecodes_cursor = self.tx_ref().cursor_write::<tables::Bytecodes>()?;
|
||||
for (hash, bytecode) in bytecodes {
|
||||
bytecodes_cursor.upsert(hash, &bytecode)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<TX: DbTx + 'static, N: NodeTypes> TryIntoHistoricalStateProvider for DatabaseProvider<TX, N> {
|
||||
@@ -2718,6 +2706,17 @@ impl<TX: DbTxMut + DbTx + 'static, N: NodeTypesForProvider> StateWriter
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_bytecodes(
|
||||
&self,
|
||||
bytecodes: impl IntoIterator<Item = (B256, Bytecode)>,
|
||||
) -> ProviderResult<()> {
|
||||
let mut bytecodes_cursor = self.tx_ref().cursor_write::<tables::Bytecodes>()?;
|
||||
for (hash, bytecode) in bytecodes {
|
||||
bytecodes_cursor.upsert(hash, &bytecode)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the last N blocks of state.
|
||||
///
|
||||
/// The latest state will be unwound
|
||||
@@ -3938,7 +3937,7 @@ mod tests {
|
||||
use alloy_consensus::Header;
|
||||
use alloy_primitives::{
|
||||
map::{AddressMap, B256Map},
|
||||
U256,
|
||||
Bytes, U256,
|
||||
};
|
||||
use reth_chain_state::ExecutedBlock;
|
||||
use reth_db_api::models::StorageSettings;
|
||||
@@ -3954,6 +3953,20 @@ mod tests {
|
||||
use revm_state::AccountInfo;
|
||||
use std::{sync::mpsc, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn write_bytecodes_writes_to_db() {
|
||||
let factory = create_test_provider_factory();
|
||||
let code_hash = B256::with_last_byte(1);
|
||||
let bytecode = Bytecode::new_raw(Bytes::from_static(&[0x60, 0x00]));
|
||||
|
||||
let provider_rw = factory.provider_rw().unwrap();
|
||||
provider_rw.write_bytecodes([(code_hash, bytecode.clone())]).unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
|
||||
let provider = factory.provider().unwrap();
|
||||
assert_eq!(provider.tx_ref().get::<tables::Bytecodes>(code_hash).unwrap(), Some(bytecode));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_receipts_by_block_range_empty_range() {
|
||||
let factory = create_test_provider_factory();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use alloc::vec::Vec;
|
||||
use alloy_consensus::transaction::Either;
|
||||
use alloy_primitives::BlockNumber;
|
||||
use alloy_primitives::{BlockNumber, B256};
|
||||
use reth_execution_types::{BlockExecutionOutput, ExecutionOutcome};
|
||||
use reth_primitives_traits::Bytecode;
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
use reth_trie_common::HashedPostStateSorted;
|
||||
use revm_database::{
|
||||
@@ -115,6 +116,12 @@ pub trait StateWriter {
|
||||
/// Writes the hashed state changes to the database
|
||||
fn write_hashed_state(&self, hashed_state: &HashedPostStateSorted) -> ProviderResult<()>;
|
||||
|
||||
/// Writes bytecodes to the database.
|
||||
fn write_bytecodes(
|
||||
&self,
|
||||
bytecodes: impl IntoIterator<Item = (B256, Bytecode)>,
|
||||
) -> ProviderResult<()>;
|
||||
|
||||
/// Remove the block range of state above the given block. The state of the passed block is not
|
||||
/// removed.
|
||||
fn remove_state_above(&self, block: BlockNumber) -> ProviderResult<()>;
|
||||
|
||||
@@ -437,7 +437,7 @@ where
|
||||
// Reject transactions with a nonce equal to U64::max according to EIP-2681
|
||||
let tx_nonce = transaction.nonce();
|
||||
if tx_nonce == u64::MAX {
|
||||
return Err(InvalidPoolTransactionError::Eip2681)
|
||||
return Err(InvalidPoolTransactionError::Eip2681);
|
||||
}
|
||||
|
||||
// Reject transactions over defined size to prevent DOS attacks
|
||||
@@ -451,7 +451,7 @@ where
|
||||
return Err(InvalidPoolTransactionError::OversizedData {
|
||||
size: tx_input_len,
|
||||
limit: self.max_tx_input_bytes,
|
||||
})
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// ensure the size of the non-blob transaction
|
||||
@@ -460,7 +460,7 @@ where
|
||||
return Err(InvalidPoolTransactionError::OversizedData {
|
||||
size: tx_size,
|
||||
limit: self.max_tx_input_bytes,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ where
|
||||
return Err(InvalidPoolTransactionError::ExceedsGasLimit(
|
||||
transaction_gas_limit,
|
||||
block_gas_limit,
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
// Check individual transaction gas limit if configured
|
||||
@@ -488,12 +488,12 @@ where
|
||||
return Err(InvalidPoolTransactionError::MaxTxGasLimitExceeded(
|
||||
transaction_gas_limit,
|
||||
max_tx_gas_limit,
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
// Ensure max_priority_fee_per_gas (if EIP1559) is less than max_fee_per_gas if any.
|
||||
if transaction.max_priority_fee_per_gas() > Some(transaction.max_fee_per_gas()) {
|
||||
return Err(InvalidTransactionError::TipAboveFeeCap.into())
|
||||
return Err(InvalidTransactionError::TipAboveFeeCap.into());
|
||||
}
|
||||
|
||||
// determine whether the transaction should be treated as local
|
||||
@@ -510,7 +510,7 @@ where
|
||||
return Err(InvalidPoolTransactionError::ExceedsFeeCap {
|
||||
max_tx_fee_wei: max_tx_fee_wei.saturating_to(),
|
||||
tx_fee_cap_wei,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -526,24 +526,24 @@ where
|
||||
minimum_priority_fee: self
|
||||
.minimum_priority_fee
|
||||
.expect("minimum priority fee is expected inside if statement"),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Checks for chainid
|
||||
if let Some(chain_id) = transaction.chain_id() &&
|
||||
chain_id != self.chain_id()
|
||||
{
|
||||
return Err(InvalidTransactionError::ChainIdMismatch.into())
|
||||
return Err(InvalidTransactionError::ChainIdMismatch.into());
|
||||
}
|
||||
|
||||
if transaction.is_eip7702() {
|
||||
// Prague fork is required for 7702 txs
|
||||
if !self.fork_tracker.is_prague_activated() {
|
||||
return Err(InvalidTransactionError::TxTypeNotSupported.into())
|
||||
return Err(InvalidTransactionError::TxTypeNotSupported.into());
|
||||
}
|
||||
|
||||
if transaction.authorization_list().is_none_or(|l| l.is_empty()) {
|
||||
return Err(Eip7702PoolTransactionError::MissingEip7702AuthorizationList.into())
|
||||
return Err(Eip7702PoolTransactionError::MissingEip7702AuthorizationList.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ where
|
||||
if transaction.is_eip4844() {
|
||||
// Cancun fork is required for blob txs
|
||||
if !self.fork_tracker.is_cancun_activated() {
|
||||
return Err(InvalidTransactionError::TxTypeNotSupported.into())
|
||||
return Err(InvalidTransactionError::TxTypeNotSupported.into());
|
||||
}
|
||||
|
||||
let blob_count = transaction.blob_count().unwrap_or(0);
|
||||
@@ -561,7 +561,7 @@ where
|
||||
// no blobs
|
||||
return Err(InvalidPoolTransactionError::Eip4844(
|
||||
Eip4844PoolTransactionError::NoEip4844Blobs,
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
let max_blob_count = self.fork_tracker.max_blob_count();
|
||||
@@ -571,7 +571,7 @@ where
|
||||
have: blob_count,
|
||||
permitted: max_blob_count,
|
||||
},
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,7 +579,7 @@ where
|
||||
let tx_gas_limit_cap =
|
||||
self.fork_tracker.tx_gas_limit_cap.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if tx_gas_limit_cap > 0 && transaction.gas_limit() > tx_gas_limit_cap {
|
||||
return Err(InvalidTransactionError::GasLimitTooHigh.into())
|
||||
return Err(InvalidTransactionError::GasLimitTooHigh.into());
|
||||
}
|
||||
|
||||
// Run additional stateless validation if configured
|
||||
@@ -622,12 +622,12 @@ where
|
||||
if transaction.requires_nonce_check() &&
|
||||
let Err(err) = self.validate_sender_nonce(&transaction, &account)
|
||||
{
|
||||
return TransactionValidationOutcome::Invalid(transaction, err)
|
||||
return TransactionValidationOutcome::Invalid(transaction, err);
|
||||
}
|
||||
|
||||
// checks for max cost not exceedng account_balance
|
||||
if let Err(err) = self.validate_sender_balance(&transaction, &account) {
|
||||
return TransactionValidationOutcome::Invalid(transaction, err)
|
||||
return TransactionValidationOutcome::Invalid(transaction, err);
|
||||
}
|
||||
|
||||
// heavy blob tx validation
|
||||
@@ -640,7 +640,7 @@ where
|
||||
if let Some(check) = &self.additional_stateful_validation &&
|
||||
let Err(err) = check(origin, &transaction, &state)
|
||||
{
|
||||
return TransactionValidationOutcome::Invalid(transaction, err)
|
||||
return TransactionValidationOutcome::Invalid(transaction, err);
|
||||
}
|
||||
|
||||
let authorities = self.recover_authorities(&transaction);
|
||||
@@ -691,7 +691,7 @@ where
|
||||
};
|
||||
|
||||
if !is_eip7702 {
|
||||
return Ok(Err(InvalidTransactionError::SignerAccountHasBytecode.into()))
|
||||
return Ok(Err(InvalidTransactionError::SignerAccountHasBytecode.into()));
|
||||
}
|
||||
}
|
||||
Ok(Ok(()))
|
||||
@@ -710,7 +710,7 @@ where
|
||||
tx: tx_nonce,
|
||||
state: sender.nonce,
|
||||
}
|
||||
.into())
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -728,7 +728,7 @@ where
|
||||
return Err(InvalidTransactionError::InsufficientFunds(
|
||||
GotExpected { got: sender.balance, expected }.into(),
|
||||
)
|
||||
.into())
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -746,7 +746,7 @@ where
|
||||
match transaction.take_blob() {
|
||||
EthBlobTransactionSidecar::None => {
|
||||
// this should not happen
|
||||
return Err(InvalidTransactionError::TxTypeNotSupported.into())
|
||||
return Err(InvalidTransactionError::TxTypeNotSupported.into());
|
||||
}
|
||||
EthBlobTransactionSidecar::Missing => {
|
||||
// This can happen for re-injected blob transactions (on re-org), since the blob
|
||||
@@ -758,7 +758,7 @@ where
|
||||
} else {
|
||||
return Err(InvalidPoolTransactionError::Eip4844(
|
||||
Eip4844PoolTransactionError::MissingEip4844BlobSidecar,
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
EthBlobTransactionSidecar::Present(sidecar) => {
|
||||
@@ -771,19 +771,19 @@ where
|
||||
if sidecar.is_eip4844() {
|
||||
return Err(InvalidPoolTransactionError::Eip4844(
|
||||
Eip4844PoolTransactionError::UnexpectedEip4844SidecarAfterOsaka,
|
||||
))
|
||||
));
|
||||
}
|
||||
} else if sidecar.is_eip7594() && !self.allow_7594_sidecars() {
|
||||
return Err(InvalidPoolTransactionError::Eip4844(
|
||||
Eip4844PoolTransactionError::UnexpectedEip7594SidecarBeforeOsaka,
|
||||
))
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// EIP-7594 disabled: always reject v1 sidecars, accept v0
|
||||
if sidecar.is_eip7594() {
|
||||
return Err(InvalidPoolTransactionError::Eip4844(
|
||||
Eip4844PoolTransactionError::Eip7594SidecarDisallowed,
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -791,7 +791,7 @@ where
|
||||
if let Err(err) = transaction.validate_blob(&sidecar, self.kzg_settings.get()) {
|
||||
return Err(InvalidPoolTransactionError::Eip4844(
|
||||
Eip4844PoolTransactionError::InvalidEip4844Blob(err),
|
||||
))
|
||||
));
|
||||
}
|
||||
// Record the duration of successful blob validation as histogram
|
||||
self.validation_metrics.blob_validation_duration.record(now.elapsed());
|
||||
@@ -1424,6 +1424,7 @@ pub fn ensure_intrinsic_gas<T: EthPoolTransaction>(
|
||||
.map(|l| l.iter().map(|i| i.storage_keys.len()).sum::<usize>())
|
||||
.unwrap_or_default() as u64,
|
||||
transaction.authorization_list().map(|l| l.len()).unwrap_or_default() as u64,
|
||||
revm_primitives::eip8037::CPSB_GLAMSTERDAM,
|
||||
);
|
||||
|
||||
let gas_limit = transaction.gas_limit();
|
||||
|
||||
@@ -5,12 +5,12 @@ use alloc::{borrow::Cow, collections::VecDeque, vec::Vec};
|
||||
use alloy_consensus::constants::KECCAK_EMPTY;
|
||||
use alloy_primitives::{
|
||||
keccak256,
|
||||
map::{hash_map, B256Map, B256Set},
|
||||
map::{hash_map, B256Map, B256Set, HashMap},
|
||||
Address, Bytes, B256, U256,
|
||||
};
|
||||
use alloy_rlp::{encode_fixed_size, Decodable, EMPTY_STRING_CODE};
|
||||
use alloy_trie::{
|
||||
nodes::TrieNode,
|
||||
nodes::{RlpNode, TrieNode},
|
||||
proof::{verify_proof, DecodedProofNodes, ProofNodes, ProofVerificationError},
|
||||
EMPTY_ROOT_HASH,
|
||||
};
|
||||
@@ -1003,9 +1003,128 @@ pub mod triehash {
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies a proof whose nodes are provided as an unordered node set.
|
||||
///
|
||||
/// Some multiproof producers return each retained trie node once, without preserving the
|
||||
/// root-to-leaf ordering expected by [`verify_proof`]. This reconstructs the single path for
|
||||
/// `key` by following child references from `root`, then delegates value verification to Alloy.
|
||||
pub fn verify_unordered_proof(
|
||||
root: B256,
|
||||
key: Nibbles,
|
||||
expected_value: Option<Vec<u8>>,
|
||||
proof: &[Bytes],
|
||||
) -> Result<(), ProofVerificationError> {
|
||||
if proof.is_empty() || proof.len() == 1 && proof[0].as_ref() == [EMPTY_STRING_CODE] {
|
||||
return verify_proof(root, key, expected_value, proof);
|
||||
}
|
||||
|
||||
let proof_by_reference = proof
|
||||
.iter()
|
||||
.map(|node| (RlpNode::from_rlp(node).as_slice().to_vec(), node))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut expected = RlpNode::word_rlp(&root);
|
||||
let mut walked = Nibbles::new();
|
||||
let mut ordered = Vec::new();
|
||||
|
||||
loop {
|
||||
let Some(node) = proof_by_reference.get(expected.as_slice()) else {
|
||||
return Err(ProofVerificationError::ValueMismatch {
|
||||
path: walked,
|
||||
got: None,
|
||||
expected: Some(Bytes::copy_from_slice(expected.as_slice())),
|
||||
});
|
||||
};
|
||||
|
||||
ordered.push((*node).clone());
|
||||
let decoded = TrieNode::decode(&mut &node[..])?;
|
||||
|
||||
match next_proof_reference(decoded, &mut walked, &key)? {
|
||||
Some(next) if next.is_hash() => expected = next,
|
||||
Some(next) => {
|
||||
let mut inline = next;
|
||||
loop {
|
||||
let decoded = TrieNode::decode(&mut inline.as_slice())?;
|
||||
match next_proof_reference(decoded, &mut walked, &key)? {
|
||||
Some(next) if next.is_hash() => {
|
||||
expected = next;
|
||||
break;
|
||||
}
|
||||
Some(next) => inline = next,
|
||||
None => return verify_proof(root, key, expected_value, &ordered),
|
||||
}
|
||||
}
|
||||
}
|
||||
None => return verify_proof(root, key, expected_value, &ordered),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_proof_reference(
|
||||
node: TrieNode,
|
||||
walked: &mut Nibbles,
|
||||
key: &Nibbles,
|
||||
) -> Result<Option<RlpNode>, ProofVerificationError> {
|
||||
match node {
|
||||
TrieNode::EmptyRoot => Err(ProofVerificationError::UnexpectedEmptyRoot),
|
||||
TrieNode::Leaf(leaf) => {
|
||||
walked.extend(&leaf.key);
|
||||
Ok(None)
|
||||
}
|
||||
TrieNode::Extension(extension) => {
|
||||
if walked.len() > key.len() {
|
||||
return Ok(None);
|
||||
}
|
||||
let remaining = key.slice(walked.len()..);
|
||||
if !remaining.starts_with(&extension.key) {
|
||||
return Ok(None);
|
||||
}
|
||||
walked.extend(&extension.key);
|
||||
Ok(Some(extension.child))
|
||||
}
|
||||
TrieNode::Branch(branch) => {
|
||||
let Some(nibble) = key.get(walked.len()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
walked.push(nibble);
|
||||
Ok(branch
|
||||
.as_ref()
|
||||
.children()
|
||||
.find(|(index, _)| *index == nibble)
|
||||
.and_then(|(_, child)| child.cloned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_trie::{proof::ProofRetainer, HashBuilder};
|
||||
|
||||
#[test]
|
||||
fn verify_unordered_proof_accepts_multiproof_node_set() {
|
||||
let key_a = Nibbles::unpack(B256::with_last_byte(1));
|
||||
let key_b = Nibbles::unpack(B256::with_last_byte(2));
|
||||
let value_a = vec![0xaa; 64];
|
||||
let value_b = vec![0xbb; 64];
|
||||
|
||||
let mut builder =
|
||||
HashBuilder::default().with_proof_retainer(ProofRetainer::new(vec![key_a, key_b]));
|
||||
builder.add_leaf(key_a, &value_a);
|
||||
builder.add_leaf(key_b, &value_b);
|
||||
|
||||
let root = builder.root();
|
||||
let mut proof = builder
|
||||
.take_proof_nodes()
|
||||
.into_nodes_sorted()
|
||||
.into_iter()
|
||||
.map(|(_, node)| node)
|
||||
.collect::<Vec<_>>();
|
||||
proof.reverse();
|
||||
|
||||
verify_unordered_proof(root, key_a, Some(value_a), &proof).unwrap();
|
||||
verify_unordered_proof(root, key_b, Some(value_b), &proof).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiproof_extend_account_proofs() {
|
||||
|
||||
395
snapv2.md
Normal file
395
snapv2.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# Snap/2: BAL-Based State Healing & Reth
|
||||
|
||||
## Background
|
||||
|
||||
### What is a BAL? (EIP-7928)
|
||||
|
||||
A **Block Access List** is a per-block structure recording every account and storage slot accessed during execution, along with their post-execution values (balance, nonce, storage writes, code changes). It's RLP-encoded and its `keccak256` hash is committed to the block header as `block_access_list_hash`. Essentially a complete, cryptographically-anchored state diff per block.
|
||||
|
||||
BALs are transmitted via the Engine API as a field in `ExecutionPayloadV4` and stored separately from block bodies. Nodes MUST retain BALs for at least the weak subjectivity period (~3533 epochs).
|
||||
|
||||
### Snap/1 Today — The Problem
|
||||
|
||||
A node syncing from scratch with snap/1:
|
||||
|
||||
1. **Download headers** via the `eth` protocol.
|
||||
2. **Pick a pivot block P** (HEAD−64).
|
||||
3. **Bulk download flat state at P** — `GetAccountRange` / `GetStorageRanges` / `GetByteCodes` retrieve leaf data (accounts, storage slots, bytecode) in hash-sorted ranges with Merkle boundary proofs. No internal trie nodes are downloaded.
|
||||
4. **Chain advances** — while downloading (hours), blocks P+1…P+K are produced. The downloaded state becomes inconsistent (some data at P, some at P+3, etc.).
|
||||
5. **Healing phase** (the bottleneck) — iteratively discover and fetch individual Merkle trie nodes via `GetTrieNodes` to fix inconsistencies. Each response reveals more missing nodes deeper in the trie. This is **unbounded and iterative** — many round trips, no upfront knowledge of how much work remains.
|
||||
6. **Verify state root** against header.
|
||||
|
||||
### Why Reth Can't Do Snap/1
|
||||
|
||||
Snap/1's `GetAccountRange` requires serving nodes to **iterate accounts in hash order at any of the last ~128 blocks**. Geth solves this with a dedicated "snapshot" layer — a flat key-value store (`keccak(address) → account`) with a diff journal to reconstruct state at recent blocks.
|
||||
|
||||
Reth has:
|
||||
- **`HashedAccounts`** (MDBX) — keyed by `keccak256(address)`, correct sort order, but only reflects **current HEAD state**. No diff journal to roll back N blocks.
|
||||
- **~2 blocks of in-memory state** via `MemoryOverlayStateProvider` — far short of the ~128 block window needed.
|
||||
|
||||
Without a Geth-style snapshot layer or equivalent, reth cannot serve snap requests.
|
||||
|
||||
## Snap/2 with BALs (EIP-8189)
|
||||
|
||||
### Protocol Changes
|
||||
|
||||
- **Removed**: `GetTrieNodes` (0x06) / `TrieNodes` (0x07)
|
||||
- **Added**: `GetBlockAccessLists` (0x08) / `BlockAccessLists` (0x09)
|
||||
- **Unchanged**: `GetAccountRange`, `AccountRange`, `GetStorageRanges`, `StorageRanges`, `GetByteCodes`, `ByteCodes` (0x00–0x05)
|
||||
|
||||
### Sync Algorithm
|
||||
|
||||
1. **Download headers and identify the chain head.** The CL sends `new_payload` / `forkchoiceUpdated` — the engine knows HEAD immediately.
|
||||
2. **Pick a pivot block P** sufficiently behind HEAD (e.g., HEAD−64) to reduce the likelihood of P being reorged while remaining recent enough that serving peers still hold its state.
|
||||
3. **Bulk download flat state at P** via `GetAccountRange` / `GetStorageRanges` / `GetByteCodes`. Write directly to `HashedAccounts` / `HashedStorages` / `Bytecodes`.
|
||||
4. **If P becomes stale during download** (serving peers no longer hold state at P — detected by empty responses mid-range), fetch BALs for blocks P+1..P' via `GetBlockAccessLists` (where P' is the new pivot), apply the diffs locally, update root_hash, and continue download at P'.
|
||||
5. **While state is being downloaded, the chain advances** and the pivot may need to switch from P to P+K. BALs for the gap are either already buffered (from CL `new_payload` events received during download) or fetched from peers. Each BAL is verified against its header's `block_access_list_hash` before application.
|
||||
6. **If the pivot advances further during step 5**, repeat for the newly produced blocks.
|
||||
7. **Recompute and verify the state root** against the latest header.
|
||||
|
||||
### Serving Snap Requests (Reth as Serving Node)
|
||||
|
||||
To serve `GetAccountRange` at a historical `rootHash` (e.g., HEAD−64), reth needs to reconstruct what `HashedAccounts` looked like at that block. With BALs this becomes feasible:
|
||||
|
||||
#### Sorted Overlay Approach
|
||||
|
||||
1. **Reverse-apply BALs** from HEAD back to the pivot block, building a `HashedPostState` overlay — a `B256Map<Option<Account>>` where `None` means "account didn't exist at pivot."
|
||||
|
||||
2. **Sort the overlay** via `.into_sorted()` → `HashedPostStateSorted`, producing a `Vec<(B256, Option<Account>)>` sorted by hash. This pattern already exists in reth for trie computation.
|
||||
|
||||
3. **Merge-iterate** the sorted overlay with the MDBX `HashedAccounts` cursor (two-pointer merge):
|
||||
|
||||
```
|
||||
MDBX cursor (HEAD state): Sorted overlay (reverse diffs):
|
||||
0x0a1b → Alice(10 ETH) 0x0a1b → Some(Alice, 12 ETH)
|
||||
0x2f3c → Bob(3 ETH) 0x2f3c → Some(Bob, 2 ETH)
|
||||
0x7d8e → Charlie(50 ETH) 0x7d8e → Some(Charlie, 45 ETH)
|
||||
0xa1b2 → Dave(7 ETH) 0xa1b2 → None (didn't exist)
|
||||
0xf4e5 → Eve(0.5 ETH) 0xf4e5 → Some(Eve, nonce 99)
|
||||
|
||||
Merge result:
|
||||
0x0a1b → Alice(12 ETH) ← overlay wins
|
||||
0x2f3c → Bob(2 ETH) ← overlay wins
|
||||
0x7d8e → Charlie(45 ETH) ← overlay wins
|
||||
0xa1b2 → SKIP ← overlay = None
|
||||
0xf4e5 → Eve(nonce 99) ← overlay wins
|
||||
(accounts not in overlay) ← read directly from MDBX
|
||||
```
|
||||
|
||||
4. **Cache the overlay** for the current pivot. Rebuild when pivot advances (~every 64 blocks / ~13 minutes).
|
||||
|
||||
#### Why This Works
|
||||
|
||||
- `HashedPostState` / `HashedPostStateSorted` already exist and are used for trie computation.
|
||||
- Merge iteration over sorted overlay + MDBX cursor is the same pattern reth uses in trie walks.
|
||||
- BALs are already in the right format — keyed by address hash with exact post-values, so reverse application is straightforward.
|
||||
- The overlay size for ~64 blocks of mainnet state changes is ~5–15 MB — easily fits in memory.
|
||||
|
||||
### Existing Infrastructure in Reth
|
||||
|
||||
| Component | Location | Relevance |
|
||||
|---|---|---|
|
||||
| `HashedPostState` | `crates/trie/common/src/hashed_state.rs` | In-memory hashed state diff (`B256Map<Option<Account>>`) |
|
||||
| `HashedPostStateSorted` | same file | Sorted variant (`Vec<(B256, Option<Account>)>`) for merge iteration |
|
||||
| `HashedStorage` / `HashedStorageSorted` | same file | Same pattern for storage slots |
|
||||
| `MemoryOverlayStateProvider` | `crates/chain-state/src/memory_overlay.rs` | Existing overlay pattern (point-lookups only, not sorted iteration) |
|
||||
| Snap wire types | `crates/net/eth-wire-types/src/snap.rs` | `GetAccountRangeMessage`, `SnapProtocolMessage`, etc. already defined |
|
||||
| `SnapClient` trait | `crates/net/p2p/src/snap/client.rs` | Client-side interface already defined |
|
||||
| `HashedAccounts` table | `crates/storage/db-api/src/tables/mod.rs` | MDBX table, hash-sorted, supports cursor iteration |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Two-Phase Design
|
||||
|
||||
Snap sync uses two complementary mechanisms:
|
||||
|
||||
1. **HeaderStage (historical)** — downloads headers 1..pivot via the existing `HeaderStage` from the staged sync pipeline. This is a one-shot batch operation that populates static files with all historical headers. Fast and well-tested.
|
||||
|
||||
2. **Engine-driven orchestrator (live)** — downloads state at the pivot, ingests blocks/headers/BALs for the range beyond the pivot as the chain advances, and verifies the final state root. This is a live, reactive process driven by FCU events from the CL.
|
||||
|
||||
### Why This Split
|
||||
|
||||
The staged sync pipeline is batch + sequential — perfect for downloading a known range of headers (1..pivot) into static files. But snap sync's state download is **live and reactive**: the chain advances during download, pivot may need to advance, and BALs must be ingested continuously. This is the engine's domain.
|
||||
|
||||
Key constraint: **during snap sync the engine returns `SYNCING` to the CL.** The CL may not send `new_payload` while the node is syncing — we can only rely on `forkchoiceUpdated` for chain advancement signals. The orchestrator must actively fetch headers and BALs from peers rather than waiting for the CL to provide them.
|
||||
|
||||
### Engine Integration
|
||||
|
||||
The engine tree already has block download primitives (`BasicBlockDownloader`, `FullBlockClient`, `DownloadRequest`) that fetch full blocks (header + body) from peers when encountering unknown block hashes. During snap sync, instead of dropping downloaded blocks (current behavior when backfill is active), the engine forwards them to the orchestrator.
|
||||
|
||||
```
|
||||
Consensus Layer
|
||||
│
|
||||
FCU(head_hash) only
|
||||
(no new_payload during SYNCING)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Engine Tree │
|
||||
│ │
|
||||
│ FCU → download │──── SnapSyncEvent::NewHead{hash}
|
||||
│ machinery fetches │ (just the hash from FCU)
|
||||
│ unknown blocks │
|
||||
│ from peers │──── Downloaded SealedBlocks
|
||||
│ │ (header + body, includes BAL?)
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Orchestrator │
|
||||
│ │
|
||||
│ Buffers blocks │
|
||||
│ Orders by number │
|
||||
│ │
|
||||
├──────────┬───────────┤
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Persistence BAL Store State Download
|
||||
(headers to (for state (GetAccountRange,
|
||||
static healing) GetStorageRanges,
|
||||
files from GetByteCodes at
|
||||
pivot+1) pivot root)
|
||||
```
|
||||
|
||||
### Orchestrator Algorithm
|
||||
|
||||
```
|
||||
Phase A — Header Pipeline (batch):
|
||||
Receive BackfillAction::StartSnapSync(target_hash)
|
||||
Resolve target_number from peers via HeadersClient
|
||||
Pick pivot P = target_number − PIVOT_OFFSET (currently 16)
|
||||
Run HeaderStage with NoopConsensus: downloads headers 1..P
|
||||
→ headers in static files, HeaderNumbers in DB, checkpoint set
|
||||
Headers 0..P now available via normal provider lookups
|
||||
|
||||
Phase B — State Download (live, engine-driven):
|
||||
Read pivot root from local header (static files)
|
||||
Clear HashedAccounts / HashedStorages / Bytecodes / trie tables
|
||||
|
||||
Bulk download at pivot:
|
||||
GetAccountRange / GetStorageRanges / GetByteCodes at pivot root
|
||||
Write directly to HashedAccounts / HashedStorages / Bytecodes
|
||||
|
||||
Concurrently, ingest blocks from engine:
|
||||
Engine receives FCU → downloads unknown blocks from peers
|
||||
Downloaded blocks forwarded to orchestrator
|
||||
Orchestrator buffers headers + BALs
|
||||
Pipes headers to persistence (extends static files from pivot+1)
|
||||
Stores BALs for state healing
|
||||
|
||||
Pivot advancement (when pivot becomes stale):
|
||||
Fetch BALs for P+1..P' (from buffer or peers via GetBlockAccessLists)
|
||||
Resolve block hashes via buffered headers or HeadersClient
|
||||
Apply BAL diffs to HashedAccounts / HashedStorages / Bytecodes
|
||||
P' becomes new pivot, continue bulk download at P'.state_root
|
||||
|
||||
Final BAL catch-up:
|
||||
Apply BALs from initial_pivot+1 to known_head (not current pivot)
|
||||
Initial pivot is the block at which bulk download started
|
||||
When pivot advances mid-download, MDBX state is mixed (some accounts at
|
||||
initial pivot, some at advanced pivot). BALs set absolute values, so
|
||||
applying all blocks from initial_pivot+1 in order heals everything.
|
||||
|
||||
Verification:
|
||||
Compute state root from HashedAccounts / HashedStorages
|
||||
Verify against header(final_block).state_root
|
||||
Report SnapSyncOutcome to engine
|
||||
```
|
||||
|
||||
### Engine → Orchestrator Protocol
|
||||
|
||||
```
|
||||
SnapSyncEvent::NewHead { head_hash: B256 }
|
||||
— sent on forkchoiceUpdated; just the hash (orchestrator resolves number from peers)
|
||||
|
||||
SnapSyncEvent::DownloadedBlock { number, hash, state_root, parent_hash }
|
||||
— forwarded from engine's block download machinery
|
||||
— during snap sync, on_downloaded_block forwards instead of inserting
|
||||
|
||||
SnapSyncOutcome { synced_to, block_hash }
|
||||
— reported back to engine on completion
|
||||
```
|
||||
|
||||
### After Snap Sync Completes
|
||||
|
||||
1. Headers 0..final_block are in static files (historical via HeaderStage, recent headers persisted via PersistenceTask during sync)
|
||||
2. Hashed state leaves (HashedAccounts, HashedStorages, Bytecodes) verified at final_block
|
||||
3. Stage checkpoints set: Execution/AccountHashing/StorageHashing/Bodies/SenderRecovery/Histories → snap target. MerkleExecute + Finish left at 0.
|
||||
4. Normal backfill pipeline triggered — only MerkleExecute + Finish run. MerkleExecute builds AccountsTrie/StoragesTrie from the hashed leaves. No state root computation needed during snap sync itself (BALs populate leaves, MerkleExecute builds the trie).
|
||||
5. Node transitions to normal live sync mode
|
||||
|
||||
### Crash Resistance
|
||||
|
||||
A "snap sync in progress" marker is written to DB before clearing hashed state. If the node crashes mid-sync:
|
||||
|
||||
1. On restart, engine finds the marker
|
||||
2. Wipes all hashed state (partial state is untrusted)
|
||||
3. Restarts snap sync from scratch
|
||||
|
||||
Resume-from-midpoint is a future optimization (would require persisting download progress: last hash range, current pivot, etc.).
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### Current State
|
||||
|
||||
The branch now has a true `snap/2` shape instead of only preparatory BAL types:
|
||||
|
||||
1. **Capability negotiation** — default hello protocols advertise `snap/2`. Snap request scheduling requires peers to advertise the matching snap version, and incoming snap payloads are decoded with the negotiated version.
|
||||
2. **Snap/2 wire decoding** — `snap/2` accepts messages 0x00-0x05 plus `GetBlockAccessLists` / `BlockAccessLists` at 0x08 / 0x09. The legacy snap/1 trie-node request/response types are not part of the branch API.
|
||||
3. **Snap leaf payload types** — `AccountData` carries `TrieAccount` internally and owns the snap slim account body codec at the wire boundary; storage value helpers live on `StorageData`; engine snap no longer carries standalone account/storage RLP encode/decode helpers.
|
||||
4. **Snap/2 BAL exchange** — `SnapClient` exposes `get_snap_block_access_lists`, `PeerRequest` has a separate `GetSnapBlockAccessLists` variant, and `SnapRequestHandler` serves BALs through the snap request path.
|
||||
5. **Snap-owned serving logic** — provider-backed snap serving lives in `reth-engine-snap::serve::ProviderSnapState`; the network crate owns only the generic `SnapStateProvider` trait and request handler wiring.
|
||||
6. **Snap-owned lifecycle logic** — Phase A / Phase B orchestration lives in `reth-engine-snap::controller::SnapSyncController`; `engine-tree` only starts/polls the controller and forwards events.
|
||||
7. **Tree snap state isolation** — `EngineApiTreeHandler` stores snap-specific state in `SnapTreeState` under `tree/snap.rs` rather than carrying loose `fresh_node` / `snap_events_tx` fields.
|
||||
8. **BAL verification** — BAL catch-up fetches missing BALs through snap/2, decodes them with alloy RLP, computes the EIP-7928 BAL hash, and compares it with the header's `block_access_list_hash` before applying state changes.
|
||||
9. **Hashed state hardening** — snap clears `HashedAccounts`, `HashedStorages`, `AccountsTrie`, `StoragesTrie`, and `Bytecodes`; hashed account/storage writes are adapted into `HashedPostStateSorted` and applied through the provider `StateWriter`, and bytecodes are written through `StateWriter::write_bytecodes`, so snap no longer writes those storage tables directly.
|
||||
|
||||
## Snap/1 Shared-Message Compliance Plan
|
||||
|
||||
EIP-8189 keeps snap/1 messages `0x00..0x05` unchanged. This branch now implements the shared account, storage, and bytecode range rules needed by snap/2, including snap slim account bodies and range proof verification.
|
||||
|
||||
### 1. Snap Account Body Encoding — implemented
|
||||
|
||||
- Replace full `TrieAccount` account bodies on the snap wire with the snap slim account body codec.
|
||||
- Encode `storage_root == EMPTY_ROOT_HASH` as RLP empty list and `code_hash == KECCAK_EMPTY` as RLP empty list, matching snap/1's account body format.
|
||||
- Keep typed conversions to/from `TrieAccount` so trie proof verification and hashed DB writes still use the canonical account representation internally.
|
||||
- Add roundtrip tests that assert empty storage/code fields use the slim encoding and decode back to canonical `TrieAccount` values.
|
||||
|
||||
### 2. Account Range Serving Rules — implemented
|
||||
|
||||
- If the serving node has the requested `rootHash`, always return a proof for `startingHash` unless the response is the entire account trie.
|
||||
- If no accounts exist in `[startingHash, limitHash)`, return the first account after `limitHash` when one exists, as required by snap/1.
|
||||
- Keep `responseBytes` as a soft cap, but ensure a non-empty servable range returns at least one account.
|
||||
- Preserve hash monotonicity in responses and include proof nodes for the requested origin plus the last returned account.
|
||||
|
||||
### 3. Account Range Verification — implemented
|
||||
|
||||
- Verify that returned account hashes are strictly increasing and start at or after the requested origin.
|
||||
- Verify proof-bearing responses with the internal snap range verifier, so the proof must establish that returned leaves are complete between the requested origin and right boundary, not just individually valid.
|
||||
- Treat empty account responses as stale only when the serving peer lacks the requested root, not as successful end-of-state unless the proof establishes the end.
|
||||
- Feed account ranges into the verifier with canonical trie account RLP values, while keeping the snap slim account body codec only at the wire boundary.
|
||||
- Reject skipped-middle account ranges with adversarial proof tests.
|
||||
|
||||
### 4. Storage Range Serving Rules — implemented
|
||||
|
||||
- If the serving node lacks the requested state root or any requested account hash, return an empty reply.
|
||||
- For multi-account storage requests, return full storage ranges for each account until the response limit is hit; only the last included account may be partial, and only then attach a proof.
|
||||
- For single-account continuation requests, honor `startingHash` / `limitHash`, attach a proof whenever the returned range is partial, and allow proof-free responses only when the entire requested storage range is returned.
|
||||
- Preserve storage slot hash ordering and skip zero-valued slots.
|
||||
|
||||
### 5. Storage Range Verification — implemented
|
||||
|
||||
- Verify every storage response is ordered and maps one-to-one to the requested account hashes.
|
||||
- When a proof is present, verify the full partial range with the internal snap range verifier and RLP-encoded storage slot values.
|
||||
- When no proof is present, recompute the full storage root for that account before trusting a complete storage range, or otherwise require a proof.
|
||||
- Reject partial-looking storage responses without proofs instead of silently writing them.
|
||||
- Continue recomputing full storage roots for proof-free complete storage ranges.
|
||||
- Ensure continuation chunks are accumulated and verified before any truncated account storage is written.
|
||||
|
||||
### 6. Bytecode Response Matching — implemented
|
||||
|
||||
- Snap/1 bytecode responses skip unavailable hashes, so the downloader must not zip response index to request index.
|
||||
- Validate each returned bytecode by `keccak256(code)` and match it to one of the requested hashes.
|
||||
- Require returned bytecodes to be a request-order subsequence while still allowing unavailable hashes to be skipped.
|
||||
- Empty bytecode responses should mean "none of these codes are available" for that request, not automatically "stale root".
|
||||
|
||||
### 7. Tests And Validation — passing locally
|
||||
|
||||
- Add unit tests for slim account encoding, account range edge cases, storage partial/full proof behavior, and bytecode hash matching.
|
||||
- Keep `run_snap_test.sh` running both active snap e2e tests: `can_snap_sync_catch_up` and `can_snap_sync_stale_pivot`.
|
||||
- After each protocol-significant change, run the focused crate tests plus `./run_snap_test.sh`.
|
||||
|
||||
Latest validation:
|
||||
|
||||
- `cargo test -p reth-eth-wire-types snap_account`
|
||||
- `cargo test -p reth-engine-snap proof`
|
||||
- `cargo test -p reth-engine-snap download::tests`
|
||||
- `cargo check -p reth-engine-snap`
|
||||
- `git diff --check`
|
||||
- `zepter`
|
||||
- `make lint-toml`
|
||||
- `./run_snap_test.sh`
|
||||
|
||||
### 8. Range Proof Hardening — implemented
|
||||
|
||||
The old endpoint checks proved that the requested origin and returned right boundary were individually valid, but that was not enough to prove there were no omitted leaves between them. The downloader now uses the private `crates/engine/snap/src/proof.rs` module:
|
||||
|
||||
- `verify_range_proof(root, origin, leaves, proof)` is generic over `(hashed_key, encoded_value)`, so account and storage ranges share the same verifier.
|
||||
- The verifier reconstructs the trie frontier from returned leaves plus proof subtrees outside the proven range, then compares the reconstructed root with the expected account/storage root.
|
||||
- Account ranges feed canonical `TrieAccount` RLP values into the verifier.
|
||||
- Storage ranges feed canonical RLP-encoded `U256` slot values into the verifier.
|
||||
- Tests cover complete boundary multiproofs, proof-free full trie responses, omitted interior leaves, empty tail proofs, and empty ranges that hide a right-side leaf.
|
||||
|
||||
### Passing E2E Tests
|
||||
|
||||
The intended snap test entrypoint is:
|
||||
|
||||
```bash
|
||||
./run_snap_test.sh
|
||||
```
|
||||
|
||||
The script runs both active snap sync E2E tests:
|
||||
|
||||
- **`can_snap_sync_catch_up`** — Node A builds 20 blocks, advances 15 more while Node B snap syncs. Tests snap download, BAL catch-up, MerkleExecute, and live sync to block 35.
|
||||
- **`can_snap_sync_stale_pivot`** — Lowers the serving lookback for the test, then builds 30 blocks so the initial pivot falls outside that window. Tests stale pivot recovery by advancing the pivot until a servable root is found.
|
||||
|
||||
`can_snap_sync_frozen_head` remains `#[ignore]`; it needs serving from in-memory state for the last unflushed blocks.
|
||||
|
||||
### Current Key Files
|
||||
|
||||
| Component | Location |
|
||||
|---|---|
|
||||
| Snap wire types + version gating | `crates/net/eth-wire-types/src/snap.rs` |
|
||||
| BAL response raw RLP container | `crates/net/eth-wire-types/src/block_access_lists.rs` |
|
||||
| Snap client trait | `crates/net/p2p/src/snap/client.rs` |
|
||||
| Snap serving trait | `crates/net/p2p/src/snap/server.rs` |
|
||||
| Snap request handler | `crates/net/network/src/snap_requests.rs` |
|
||||
| Snap request routing | `crates/net/network-api/src/events.rs`, `crates/net/network/src/{fetch,state,manager}.rs` |
|
||||
| Session-level snap/2 encode/decode | `crates/net/network/src/session/active.rs` |
|
||||
| Provider-backed snap serving | `crates/engine/snap/src/serve.rs` |
|
||||
| Snap range proof verification | `crates/engine/snap/src/proof.rs` |
|
||||
| Snap sync controller | `crates/engine/snap/src/controller.rs` |
|
||||
| Snap orchestrator | `crates/engine/snap/src/orchestrator.rs` |
|
||||
| BAL pivot/catch-up logic | `crates/engine/snap/src/pivot.rs`, `crates/engine/snap/src/bal.rs` |
|
||||
| Hashed state writes | `crates/engine/snap/src/storage.rs` |
|
||||
| Engine tree snap integration | `crates/engine/tree/src/tree/snap.rs` |
|
||||
| Backfill integration | `crates/engine/tree/src/backfill.rs` |
|
||||
| Node network wiring | `crates/node/builder/src/builder/mod.rs` |
|
||||
| E2E test script | `run_snap_test.sh` |
|
||||
|
||||
### Completed Refactor
|
||||
|
||||
| Area | Current status |
|
||||
|---|---|
|
||||
| Snap provider location | Moved from `crates/node/builder/src/snap_provider.rs` to `crates/engine/snap/src/serve.rs` |
|
||||
| Snap server trait | Moved to `reth-network-p2p` as `snap::server::SnapStateProvider` |
|
||||
| Snap controller location | Moved out of `engine-tree/backfill.rs` into `reth-engine-snap::controller` |
|
||||
| Engine tree fields | Collapsed snap-specific fields into `SnapTreeState` |
|
||||
| True snap/2 client path | Added `get_snap_block_access_lists` and network routing for snap/2 BAL requests |
|
||||
| Trie-node path | Removed legacy `GetTrieNodes` / `TrieNodes` wire types, client methods, request scheduling/serving, and the successful `SnapResponse::TrieNodes` path; this branch is snap/2-only |
|
||||
| BAL missing-entry handling | snap/2 serving returns RLP empty string (`0x80`) for missing BALs; eth/71 keeps empty list (`0xc0`) |
|
||||
| Hashed state write path | Snap batch shapes are converted into `HashedPostStateSorted` and written through `StateWriter::write_hashed_state`; bytecode batches use `StateWriter::write_bytecodes` |
|
||||
| Combined backfill bounds | `CombinedBackfillSync` now depends on the snap crate's `SnapSyncControl` adapter, so client/provider bounds stay out of the combined sync impls |
|
||||
| Test entrypoint | `run_snap_test.sh` now runs both active snap sync E2E tests |
|
||||
| Snap account/storage proofs | Provider-backed serving now emits boundary proof nodes for account ranges and partial storage ranges; the downloader reconstructs the proven range frontier and rejects omitted interior leaves before writing data |
|
||||
| Served account storage roots | `AccountRange` bodies now carry storage roots computed from the same historical hashed-state overlay used to serve the account and storage leaves |
|
||||
|
||||
### Remaining Debt
|
||||
|
||||
- **No crash resume.** If the node crashes mid-snap-sync, it should wipe partial hashed state and restart. Resume-from-midpoint would require persisting range cursor, pivot, and downloaded bytecode/account progress.
|
||||
- **No reorg handling during snap sync.** If a reorg crosses the pivot, the current implementation does not collect old-fork BALs and re-fetch affected ranges as EIP-8189 describes.
|
||||
- **Frozen-head serving is still limited.** Provider-backed serving reconstructs recently persisted state with changesets; it does not yet serve the final unflushed in-memory blocks.
|
||||
- **BAL cache placement remains debatable.** `BalStoreHandle` abstracts the store, but some implementation still lives near Engine API/RPC concerns. Moving shared BAL cache implementation into storage/provider may be cleaner later.
|
||||
|
||||
### Future Work
|
||||
|
||||
- **Trie population from verified proofs:** optionally insert verified proof nodes into `AccountsTrie`/`StoragesTrie` to reduce MerkleExecute work.
|
||||
- **Historical root serving cache:** Cache `HashedPostStateSorted` overlays per served pivot so repeated account/storage range requests do not rebuild reverse diffs.
|
||||
- **Peer reliability tracking for BALs:** Peers returning `0x80` for available BALs should be deprioritized separately from ordinary empty snap state responses.
|
||||
- **Reorg handling during snap sync:** Implement the EIP-8189 old-fork/new-fork BAL procedure, and restart if required orphaned BALs are unavailable.
|
||||
|
||||
### References
|
||||
|
||||
- [EIP-7928: Block-Level Access Lists](https://eips.ethereum.org/EIPS/eip-7928)
|
||||
- [EIP-8189: snap/2 — BAL-Based State Healing](https://eips.ethereum.org/EIPS/eip-8189)
|
||||
- [Snap/1 Protocol Spec](https://github.com/ethereum/devp2p/blob/master/caps/snap.md)
|
||||
@@ -252,7 +252,7 @@ fn run_case(case: &BlockchainTest) -> Result<(), Error> {
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
// Consensus checks after block execution
|
||||
validate_block_post_execution(block, &chain_spec, &output, None)
|
||||
validate_block_post_execution(block, &chain_spec, &output, None, None)
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
// Compute and check the post state root
|
||||
|
||||
Reference in New Issue
Block a user