mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
33 Commits
push
...
small-bloc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8147f4faa | ||
|
|
bf2380b099 | ||
|
|
d49f828998 | ||
|
|
2f78bcd7b5 | ||
|
|
f60febfa62 | ||
|
|
317f858bd4 | ||
|
|
11acd97982 | ||
|
|
f5cf90227b | ||
|
|
0dd47af250 | ||
|
|
0142769191 | ||
|
|
e1dc93e24f | ||
|
|
33ac869a85 | ||
|
|
ec982f8686 | ||
|
|
47cef33a0d | ||
|
|
9529de4cf2 | ||
|
|
5a9dd02301 | ||
|
|
d71a0c0c7b | ||
|
|
2be3788481 | ||
|
|
adbec3218d | ||
|
|
2e5560b444 | ||
|
|
1f3fd5da2e | ||
|
|
3ab7cb98aa | ||
|
|
d3088e171c | ||
|
|
2c443a3dcb | ||
|
|
4b444069a5 | ||
|
|
25d371817a | ||
|
|
4b0fa8a330 | ||
|
|
df22d38224 | ||
|
|
e4ec836a46 | ||
|
|
d3c42fc718 | ||
|
|
8171cee927 | ||
|
|
61cfcd8195 | ||
|
|
b646f4559c |
5
.changelog/fast-seals-play.md
Normal file
5
.changelog/fast-seals-play.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-transaction-pool: minor
|
||||
---
|
||||
|
||||
Added `consensus_ref` method to `PoolTransaction` trait for borrowing consensus transactions without cloning.
|
||||
6
.changelog/merry-koalas-nod.md
Normal file
6
.changelog/merry-koalas-nod.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
reth-rpc-eth-api: minor
|
||||
reth-rpc-server-types: minor
|
||||
---
|
||||
|
||||
Added `eth_getStorageValues` RPC method for batch storage slot retrieval across multiple addresses.
|
||||
5
.changelog/proud-wolves-spin.md
Normal file
5
.changelog/proud-wolves-spin.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
reth-storage-api: patch
|
||||
---
|
||||
|
||||
Added `Arc` to `auto_impl` derive for storage-api traits to support automatic `Arc` wrapper implementations.
|
||||
@@ -1,6 +1,6 @@
|
||||
[profile.default]
|
||||
retries = { backoff = "exponential", count = 2, delay = "2s", jitter = true }
|
||||
slow-timeout = { period = "30s", terminate-after = 4 }
|
||||
slow-timeout = { period = "30s", terminate-after = 2 }
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = "test(general_state_tests)"
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -124,7 +124,7 @@ jobs:
|
||||
- uses: rui314/setup-mold@v1
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "1.88" # MSRV
|
||||
toolchain: "1.93" # MSRV
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -102,7 +102,7 @@ jobs:
|
||||
- name: Install cross main
|
||||
id: cross_main
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
cargo install cross --locked --git https://github.com/cross-rs/cross
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
|
||||
2
.github/workflows/stage.yml
vendored
2
.github/workflows/stage.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
cache-on-failure: true
|
||||
- name: Build reth
|
||||
run: |
|
||||
cargo install --path bin/reth
|
||||
cargo install --locked --path bin/reth
|
||||
- name: Run headers stage
|
||||
run: |
|
||||
reth stage run headers --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints
|
||||
|
||||
330
Cargo.lock
generated
330
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
[workspace.package]
|
||||
version = "1.11.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
rust-version = "1.93"
|
||||
license = "MIT OR Apache-2.0"
|
||||
homepage = "https://paradigmxyz.github.io/reth"
|
||||
repository = "https://github.com/paradigmxyz/reth"
|
||||
@@ -27,7 +27,6 @@ members = [
|
||||
"crates/engine/invalid-block-hooks/",
|
||||
"crates/engine/local",
|
||||
"crates/engine/primitives/",
|
||||
"crates/engine/service",
|
||||
"crates/engine/tree/",
|
||||
"crates/engine/util/",
|
||||
"crates/era",
|
||||
@@ -349,7 +348,6 @@ reth-ecies = { path = "crates/net/ecies" }
|
||||
reth-engine-local = { path = "crates/engine/local" }
|
||||
reth-engine-primitives = { path = "crates/engine/primitives", default-features = false }
|
||||
reth-engine-tree = { path = "crates/engine/tree" }
|
||||
reth-engine-service = { path = "crates/engine/service" }
|
||||
reth-engine-util = { path = "crates/engine/util" }
|
||||
reth-era = { path = "crates/era" }
|
||||
reth-era-downloader = { path = "crates/era-downloader" }
|
||||
@@ -665,6 +663,7 @@ cipher = "0.4.3"
|
||||
comfy-table = "7.0"
|
||||
concat-kdf = "0.1.0"
|
||||
crossbeam-channel = "0.5.13"
|
||||
crossbeam-utils = "0.8"
|
||||
crossterm = "0.29.0"
|
||||
csv = "1.3.0"
|
||||
ctrlc = "3.4"
|
||||
|
||||
@@ -19,10 +19,11 @@ pre-build = [
|
||||
image = "ubuntu:24.04"
|
||||
pre-build = [
|
||||
"apt update",
|
||||
"apt install --yes gcc gcc-riscv64-linux-gnu libclang-dev make",
|
||||
"apt install --yes gcc gcc-riscv64-linux-gnu g++-riscv64-linux-gnu libclang-dev make",
|
||||
]
|
||||
env.passthrough = [
|
||||
"CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER=riscv64-linux-gnu-gcc",
|
||||
"CXX_riscv64gc_unknown_linux_gnu=riscv64-linux-gnu-g++",
|
||||
]
|
||||
|
||||
[build.env]
|
||||
|
||||
4
Makefile
4
Makefile
@@ -80,7 +80,7 @@ build-native-%:
|
||||
#
|
||||
# These commands require that:
|
||||
#
|
||||
# - `cross` is installed (`cargo install cross`).
|
||||
# - `cross` is installed (`cargo install --locked cross`).
|
||||
# - Docker is running.
|
||||
# - The current user is in the `docker` group.
|
||||
#
|
||||
@@ -261,7 +261,7 @@ lint-typos: ensure-typos
|
||||
|
||||
ensure-typos:
|
||||
@if ! command -v typos &> /dev/null; then \
|
||||
echo "typos not found. Please install it by running the command 'cargo install typos-cli' or refer to the following link for more information: https://github.com/crate-ci/typos"; \
|
||||
echo "typos not found. Please install it by running the command 'cargo install --locked typos-cli' or refer to the following link for more information: https://github.com/crate-ci/typos"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ When updating this, also update:
|
||||
- .github/workflows/lint.yml
|
||||
-->
|
||||
|
||||
The Minimum Supported Rust Version (MSRV) of this project is [1.88.0](https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/).
|
||||
The Minimum Supported Rust Version (MSRV) of this project is [1.93.0](https://blog.rust-lang.org/2026/01/22/Rust-1.93.0/).
|
||||
|
||||
See the docs for detailed instructions on how to [build from source](https://reth.rs/installation/source/).
|
||||
|
||||
|
||||
@@ -285,7 +285,6 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
|
||||
// (We can't just use `upsert` method with a dup cursor, it's not properly
|
||||
// supported)
|
||||
let nibbles = StoredNibblesSubKey(path);
|
||||
let entry = StorageTrieEntry { nibbles: nibbles.clone(), node };
|
||||
if storage_trie_cursor
|
||||
.seek_by_key_subkey(account, nibbles.clone())?
|
||||
.filter(|v| v.nibbles == nibbles)
|
||||
@@ -293,6 +292,7 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
|
||||
{
|
||||
storage_trie_cursor.delete_current()?;
|
||||
}
|
||||
let entry = StorageTrieEntry { nibbles, node };
|
||||
storage_trie_cursor.upsert(account, &entry)?;
|
||||
}
|
||||
Output::Progress(path) => {
|
||||
|
||||
@@ -54,12 +54,20 @@ impl<T: PayloadTypes> PayloadTestContext<T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wait until the best built payload is ready
|
||||
/// Wait until the best built payload is ready.
|
||||
///
|
||||
/// Panics if the payload builder does not produce a non-empty payload within 30 seconds.
|
||||
pub async fn wait_for_built_payload(&self, payload_id: PayloadId) {
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
let payload =
|
||||
self.payload_builder.best_payload(payload_id).await.transpose().ok().flatten();
|
||||
if payload.is_none_or(|p| p.block().body().transactions().is_empty()) {
|
||||
assert!(
|
||||
start.elapsed() < std::time::Duration::from_secs(30),
|
||||
"timed out waiting for a non-empty payload for {payload_id} — \
|
||||
check that the chain spec supports all generated tx types"
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -42,18 +42,6 @@ pub const DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE_V2: usize = DEFAULT_MULTIPROOF_TASK
|
||||
/// This will be deducted from the thread count of main reth global threadpool.
|
||||
pub const DEFAULT_RESERVED_CPU_CORES: usize = 1;
|
||||
|
||||
/// Returns the default maximum concurrency for prewarm task based on available parallelism.
|
||||
fn default_prewarm_max_concurrency() -> usize {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
std::thread::available_parallelism().map_or(16, |n| n.get())
|
||||
}
|
||||
#[cfg(not(feature = "std"))]
|
||||
{
|
||||
16
|
||||
}
|
||||
}
|
||||
|
||||
/// Default depth for sparse trie pruning.
|
||||
///
|
||||
/// Nodes at this depth and below are converted to hash stubs to reduce memory.
|
||||
@@ -161,8 +149,6 @@ pub struct TreeConfig {
|
||||
/// where immediate payload regeneration is desired despite the head not changing or moving to
|
||||
/// an ancestor.
|
||||
always_process_payload_attributes_on_canonical_head: bool,
|
||||
/// Maximum concurrency for the prewarm task.
|
||||
prewarm_max_concurrency: usize,
|
||||
/// Whether to unwind canonical header to ancestor during forkchoice updates.
|
||||
allow_unwind_canonical_header: bool,
|
||||
/// Number of storage proof worker threads.
|
||||
@@ -209,7 +195,6 @@ impl Default for TreeConfig {
|
||||
precompile_cache_disabled: false,
|
||||
state_root_fallback: false,
|
||||
always_process_payload_attributes_on_canonical_head: false,
|
||||
prewarm_max_concurrency: default_prewarm_max_concurrency(),
|
||||
allow_unwind_canonical_header: false,
|
||||
storage_worker_count: default_storage_worker_count(),
|
||||
account_worker_count: default_account_worker_count(),
|
||||
@@ -246,7 +231,6 @@ impl TreeConfig {
|
||||
precompile_cache_disabled: bool,
|
||||
state_root_fallback: bool,
|
||||
always_process_payload_attributes_on_canonical_head: bool,
|
||||
prewarm_max_concurrency: usize,
|
||||
allow_unwind_canonical_header: bool,
|
||||
storage_worker_count: usize,
|
||||
account_worker_count: usize,
|
||||
@@ -275,7 +259,6 @@ impl TreeConfig {
|
||||
precompile_cache_disabled,
|
||||
state_root_fallback,
|
||||
always_process_payload_attributes_on_canonical_head,
|
||||
prewarm_max_concurrency,
|
||||
allow_unwind_canonical_header,
|
||||
storage_worker_count,
|
||||
account_worker_count,
|
||||
@@ -533,17 +516,6 @@ impl TreeConfig {
|
||||
self.has_enough_parallelism && !self.legacy_state_root
|
||||
}
|
||||
|
||||
/// Setter for prewarm max concurrency.
|
||||
pub const fn with_prewarm_max_concurrency(mut self, prewarm_max_concurrency: usize) -> Self {
|
||||
self.prewarm_max_concurrency = prewarm_max_concurrency;
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the prewarm max concurrency.
|
||||
pub const fn prewarm_max_concurrency(&self) -> usize {
|
||||
self.prewarm_max_concurrency
|
||||
}
|
||||
|
||||
/// Return the number of storage proof worker threads.
|
||||
pub const fn storage_worker_count(&self) -> usize {
|
||||
self.storage_worker_count
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
[package]
|
||||
name = "reth-engine-service"
|
||||
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-consensus.workspace = true
|
||||
reth-engine-tree.workspace = true
|
||||
reth-evm.workspace = true
|
||||
reth-network-p2p.workspace = true
|
||||
reth-payload-builder.workspace = true
|
||||
reth-provider.workspace = true
|
||||
reth-prune.workspace = true
|
||||
reth-stages-api.workspace = true
|
||||
reth-tasks.workspace = true
|
||||
reth-node-types.workspace = true
|
||||
reth-chainspec.workspace = true
|
||||
reth-engine-primitives.workspace = true
|
||||
reth-trie-db.workspace = true
|
||||
|
||||
# async
|
||||
futures.workspace = true
|
||||
pin-project.workspace = true
|
||||
|
||||
# misc
|
||||
|
||||
[dev-dependencies]
|
||||
reth-engine-tree = { workspace = true, features = ["test-utils"] }
|
||||
reth-ethereum-consensus.workspace = true
|
||||
reth-ethereum-engine-primitives.workspace = true
|
||||
reth-evm-ethereum.workspace = true
|
||||
reth-exex-types.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-node-ethereum.workspace = true
|
||||
reth-trie-db.workspace = true
|
||||
|
||||
alloy-eips.workspace = true
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tokio-stream.workspace = true
|
||||
@@ -1,12 +0,0 @@
|
||||
//! Engine service implementation.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
|
||||
html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
|
||||
issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
|
||||
|
||||
/// Engine Service
|
||||
pub mod service;
|
||||
@@ -1,229 +0,0 @@
|
||||
use futures::{Stream, StreamExt};
|
||||
use pin_project::pin_project;
|
||||
use reth_chainspec::EthChainSpec;
|
||||
use reth_consensus::FullConsensus;
|
||||
use reth_engine_primitives::{BeaconEngineMessage, ConsensusEngineEvent};
|
||||
use reth_engine_tree::{
|
||||
backfill::PipelineSync,
|
||||
download::BasicBlockDownloader,
|
||||
engine::{EngineApiKind, EngineApiRequest, EngineApiRequestHandler, EngineHandler},
|
||||
persistence::PersistenceHandle,
|
||||
tree::{EngineApiTreeHandler, EngineValidator, TreeConfig},
|
||||
};
|
||||
pub use reth_engine_tree::{
|
||||
chain::{ChainEvent, ChainOrchestrator},
|
||||
engine::EngineApiEvent,
|
||||
};
|
||||
use reth_evm::ConfigureEvm;
|
||||
use reth_network_p2p::BlockClient;
|
||||
use reth_node_types::{BlockTy, NodeTypes};
|
||||
use reth_payload_builder::PayloadBuilderHandle;
|
||||
use reth_provider::{
|
||||
providers::{BlockchainProvider, ProviderNodeTypes},
|
||||
ProviderFactory, StorageSettingsCache,
|
||||
};
|
||||
use reth_prune::PrunerWithFactory;
|
||||
use reth_stages_api::{MetricEventsSender, Pipeline};
|
||||
use reth_tasks::TaskSpawner;
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
/// Alias for consensus engine stream.
|
||||
pub type EngineMessageStream<T> = Pin<Box<dyn Stream<Item = BeaconEngineMessage<T>> + Send + Sync>>;
|
||||
|
||||
/// Alias for chain orchestrator.
|
||||
type EngineServiceType<N, Client> = ChainOrchestrator<
|
||||
EngineHandler<
|
||||
EngineApiRequestHandler<
|
||||
EngineApiRequest<<N as NodeTypes>::Payload, <N as NodeTypes>::Primitives>,
|
||||
<N as NodeTypes>::Primitives,
|
||||
>,
|
||||
EngineMessageStream<<N as NodeTypes>::Payload>,
|
||||
BasicBlockDownloader<Client, BlockTy<N>>,
|
||||
>,
|
||||
PipelineSync<N>,
|
||||
>;
|
||||
|
||||
/// The type that drives the chain forward and communicates progress.
|
||||
#[pin_project]
|
||||
#[expect(missing_debug_implementations)]
|
||||
// TODO(mattsse): remove hidden once fixed : <https://github.com/rust-lang/rust/issues/135363>
|
||||
// otherwise rustdoc fails to resolve the alias
|
||||
#[doc(hidden)]
|
||||
pub struct EngineService<N, Client>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
Client: BlockClient<Block = BlockTy<N>> + 'static,
|
||||
{
|
||||
orchestrator: EngineServiceType<N, Client>,
|
||||
}
|
||||
|
||||
impl<N, Client> EngineService<N, Client>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
Client: BlockClient<Block = BlockTy<N>> + 'static,
|
||||
{
|
||||
/// Constructor for `EngineService`.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn new<V, C>(
|
||||
consensus: Arc<dyn FullConsensus<N::Primitives>>,
|
||||
chain_spec: Arc<N::ChainSpec>,
|
||||
client: Client,
|
||||
incoming_requests: EngineMessageStream<N::Payload>,
|
||||
pipeline: Pipeline<N>,
|
||||
pipeline_task_spawner: Box<dyn TaskSpawner>,
|
||||
provider: ProviderFactory<N>,
|
||||
blockchain_db: BlockchainProvider<N>,
|
||||
pruner: PrunerWithFactory<ProviderFactory<N>>,
|
||||
payload_builder: PayloadBuilderHandle<N::Payload>,
|
||||
payload_validator: V,
|
||||
tree_config: TreeConfig,
|
||||
sync_metrics_tx: MetricEventsSender,
|
||||
evm_config: C,
|
||||
changeset_cache: ChangesetCache,
|
||||
) -> Self
|
||||
where
|
||||
V: EngineValidator<N::Payload>,
|
||||
C: ConfigureEvm<Primitives = N::Primitives> + 'static,
|
||||
{
|
||||
let engine_kind =
|
||||
if chain_spec.is_optimism() { EngineApiKind::OpStack } else { EngineApiKind::Ethereum };
|
||||
|
||||
let downloader = BasicBlockDownloader::new(client, consensus.clone());
|
||||
let use_hashed_state = provider.cached_storage_settings().use_hashed_state();
|
||||
|
||||
let persistence_handle =
|
||||
PersistenceHandle::<N::Primitives>::spawn_service(provider, pruner, sync_metrics_tx);
|
||||
|
||||
let canonical_in_memory_state = blockchain_db.canonical_in_memory_state();
|
||||
|
||||
let (to_tree_tx, from_tree) = EngineApiTreeHandler::spawn_new(
|
||||
blockchain_db,
|
||||
consensus,
|
||||
payload_validator,
|
||||
persistence_handle,
|
||||
payload_builder,
|
||||
canonical_in_memory_state,
|
||||
tree_config,
|
||||
engine_kind,
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
use_hashed_state,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
Self { orchestrator: ChainOrchestrator::new(handler, backfill_sync) }
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the orchestrator.
|
||||
pub fn orchestrator_mut(&mut self) -> &mut EngineServiceType<N, Client> {
|
||||
&mut self.orchestrator
|
||||
}
|
||||
}
|
||||
|
||||
impl<N, Client> Stream for EngineService<N, Client>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
Client: BlockClient<Block = BlockTy<N>> + 'static,
|
||||
{
|
||||
type Item = ChainEvent<ConsensusEngineEvent<N::Primitives>>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut orchestrator = self.project().orchestrator;
|
||||
StreamExt::poll_next_unpin(&mut orchestrator, cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use reth_chainspec::{ChainSpecBuilder, MAINNET};
|
||||
use reth_engine_primitives::{BeaconEngineMessage, NoopInvalidBlockHook};
|
||||
use reth_engine_tree::{test_utils::TestPipelineBuilder, tree::BasicEngineValidator};
|
||||
use reth_ethereum_consensus::EthBeaconConsensus;
|
||||
use reth_ethereum_engine_primitives::EthEngineTypes;
|
||||
use reth_evm_ethereum::EthEvmConfig;
|
||||
use reth_exex_types::FinishedExExHeight;
|
||||
use reth_network_p2p::test_utils::TestFullBlockClient;
|
||||
use reth_node_ethereum::EthereumEngineValidator;
|
||||
use reth_primitives_traits::SealedHeader;
|
||||
use reth_provider::{
|
||||
providers::BlockchainProvider, test_utils::create_test_provider_factory_with_chain_spec,
|
||||
};
|
||||
use reth_prune::Pruner;
|
||||
use reth_tasks::TokioTaskExecutor;
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc::unbounded_channel, watch};
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
#[test]
|
||||
fn eth_chain_orchestrator_build() {
|
||||
let chain_spec = Arc::new(
|
||||
ChainSpecBuilder::default()
|
||||
.chain(MAINNET.chain)
|
||||
.genesis(MAINNET.genesis.clone())
|
||||
.paris_activated()
|
||||
.build(),
|
||||
);
|
||||
let consensus = Arc::new(EthBeaconConsensus::new(chain_spec.clone()));
|
||||
|
||||
let client = TestFullBlockClient::default();
|
||||
|
||||
let (_tx, rx) = unbounded_channel::<BeaconEngineMessage<EthEngineTypes>>();
|
||||
let incoming_requests = UnboundedReceiverStream::new(rx);
|
||||
|
||||
let pipeline = TestPipelineBuilder::new().build(chain_spec.clone());
|
||||
let pipeline_task_spawner = Box::<TokioTaskExecutor>::default();
|
||||
let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone());
|
||||
|
||||
let blockchain_db =
|
||||
BlockchainProvider::with_latest(provider_factory.clone(), SealedHeader::default())
|
||||
.unwrap();
|
||||
let engine_payload_validator = EthereumEngineValidator::new(chain_spec.clone());
|
||||
let (_tx, rx) = watch::channel(FinishedExExHeight::NoExExs);
|
||||
let pruner = Pruner::new_with_factory(provider_factory.clone(), vec![], 0, 0, None, rx);
|
||||
let evm_config = EthEvmConfig::new(chain_spec.clone());
|
||||
|
||||
let changeset_cache = ChangesetCache::new();
|
||||
|
||||
let engine_validator = BasicEngineValidator::new(
|
||||
blockchain_db.clone(),
|
||||
consensus.clone(),
|
||||
evm_config.clone(),
|
||||
engine_payload_validator,
|
||||
TreeConfig::default(),
|
||||
Box::new(NoopInvalidBlockHook::default()),
|
||||
changeset_cache.clone(),
|
||||
reth_tasks::Runtime::test(),
|
||||
);
|
||||
|
||||
let (sync_metrics_tx, _sync_metrics_rx) = unbounded_channel();
|
||||
let (tx, _rx) = unbounded_channel();
|
||||
let _eth_service = EngineService::new(
|
||||
consensus,
|
||||
chain_spec,
|
||||
client,
|
||||
Box::pin(incoming_requests),
|
||||
pipeline,
|
||||
pipeline_task_spawner,
|
||||
provider_factory,
|
||||
blockchain_db,
|
||||
pruner,
|
||||
PayloadBuilderHandle::new(tx),
|
||||
engine_validator,
|
||||
TreeConfig::default(),
|
||||
sync_metrics_tx,
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ reth-provider.workspace = true
|
||||
reth-prune.workspace = true
|
||||
reth-revm = { workspace = true, features = ["optional-balance-check"] }
|
||||
reth-stages-api.workspace = true
|
||||
reth-tasks.workspace = true
|
||||
reth-tasks = { workspace = true, features = ["rayon"] }
|
||||
reth-trie-parallel.workspace = true
|
||||
reth-trie-sparse = { workspace = true, features = ["std", "metrics"] }
|
||||
reth-trie.workspace = true
|
||||
|
||||
110
crates/engine/tree/src/launch.rs
Normal file
110
crates/engine/tree/src/launch.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
//! Engine orchestrator launch helper.
|
||||
//!
|
||||
//! Provides [`build_engine_orchestrator`](crate::launch::build_engine_orchestrator) which wires
|
||||
//! together all engine components and returns a
|
||||
//! [`ChainOrchestrator`](crate::chain::ChainOrchestrator) ready to be polled as a `Stream`.
|
||||
|
||||
use crate::{
|
||||
backfill::PipelineSync,
|
||||
chain::ChainOrchestrator,
|
||||
download::BasicBlockDownloader,
|
||||
engine::{EngineApiKind, EngineApiRequest, EngineApiRequestHandler, EngineHandler},
|
||||
persistence::PersistenceHandle,
|
||||
tree::{EngineApiTreeHandler, EngineValidator, TreeConfig},
|
||||
};
|
||||
use futures::Stream;
|
||||
use reth_consensus::FullConsensus;
|
||||
use reth_engine_primitives::BeaconEngineMessage;
|
||||
use reth_evm::ConfigureEvm;
|
||||
use reth_network_p2p::BlockClient;
|
||||
use reth_payload_builder::PayloadBuilderHandle;
|
||||
use reth_primitives_traits::NodePrimitives;
|
||||
use reth_provider::{
|
||||
providers::{BlockchainProvider, ProviderNodeTypes},
|
||||
ProviderFactory, StorageSettingsCache,
|
||||
};
|
||||
use reth_prune::PrunerWithFactory;
|
||||
use reth_stages_api::{MetricEventsSender, Pipeline};
|
||||
use reth_tasks::TaskSpawner;
|
||||
use reth_trie_db::ChangesetCache;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Builds the engine [`ChainOrchestrator`] that drives the chain forward.
|
||||
///
|
||||
/// This spawns and wires together the following components:
|
||||
///
|
||||
/// - **[`BasicBlockDownloader`]** — downloads blocks on demand from the network during live sync.
|
||||
/// - **[`PersistenceHandle`]** — spawns the persistence service on a background thread for writing
|
||||
/// blocks and performing pruning outside the critical consensus path.
|
||||
/// - **[`EngineApiTreeHandler`]** — spawns the tree handler that processes engine API requests
|
||||
/// (`newPayload`, `forkchoiceUpdated`) and maintains the in-memory chain state.
|
||||
/// - **[`EngineApiRequestHandler`]** + **[`EngineHandler`]** — glue that routes incoming CL
|
||||
/// messages to the tree handler and manages download requests.
|
||||
/// - **[`PipelineSync`]** — wraps the staged sync [`Pipeline`] for backfill sync when the node
|
||||
/// needs to catch up over large block ranges.
|
||||
///
|
||||
/// The returned orchestrator implements [`Stream`] and yields
|
||||
/// [`ChainEvent`]s.
|
||||
///
|
||||
/// [`ChainEvent`]: crate::chain::ChainEvent
|
||||
#[expect(clippy::too_many_arguments, clippy::type_complexity)]
|
||||
pub fn build_engine_orchestrator<N, Client, S, V, C>(
|
||||
engine_kind: EngineApiKind,
|
||||
consensus: Arc<dyn FullConsensus<N::Primitives>>,
|
||||
client: Client,
|
||||
incoming_requests: S,
|
||||
pipeline: Pipeline<N>,
|
||||
pipeline_task_spawner: Box<dyn TaskSpawner>,
|
||||
provider: ProviderFactory<N>,
|
||||
blockchain_db: BlockchainProvider<N>,
|
||||
pruner: PrunerWithFactory<ProviderFactory<N>>,
|
||||
payload_builder: PayloadBuilderHandle<N::Payload>,
|
||||
payload_validator: V,
|
||||
tree_config: TreeConfig,
|
||||
sync_metrics_tx: MetricEventsSender,
|
||||
evm_config: C,
|
||||
changeset_cache: ChangesetCache,
|
||||
) -> ChainOrchestrator<
|
||||
EngineHandler<
|
||||
EngineApiRequestHandler<EngineApiRequest<N::Payload, N::Primitives>, N::Primitives>,
|
||||
S,
|
||||
BasicBlockDownloader<Client, <N::Primitives as NodePrimitives>::Block>,
|
||||
>,
|
||||
PipelineSync<N>,
|
||||
>
|
||||
where
|
||||
N: ProviderNodeTypes,
|
||||
Client: BlockClient<Block = <N::Primitives as NodePrimitives>::Block> + 'static,
|
||||
S: Stream<Item = BeaconEngineMessage<N::Payload>> + Send + Sync + Unpin + 'static,
|
||||
V: EngineValidator<N::Payload>,
|
||||
C: ConfigureEvm<Primitives = N::Primitives> + 'static,
|
||||
{
|
||||
let downloader = BasicBlockDownloader::new(client, consensus.clone());
|
||||
let use_hashed_state = provider.cached_storage_settings().use_hashed_state();
|
||||
|
||||
let persistence_handle =
|
||||
PersistenceHandle::<N::Primitives>::spawn_service(provider, pruner, sync_metrics_tx);
|
||||
|
||||
let canonical_in_memory_state = blockchain_db.canonical_in_memory_state();
|
||||
|
||||
let (to_tree_tx, from_tree) = EngineApiTreeHandler::spawn_new(
|
||||
blockchain_db,
|
||||
consensus,
|
||||
payload_validator,
|
||||
persistence_handle,
|
||||
payload_builder,
|
||||
canonical_in_memory_state,
|
||||
tree_config,
|
||||
engine_kind,
|
||||
evm_config,
|
||||
changeset_cache,
|
||||
use_hashed_state,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
ChainOrchestrator::new(handler, backfill_sync)
|
||||
}
|
||||
@@ -100,6 +100,8 @@ pub mod chain;
|
||||
pub mod download;
|
||||
/// Engine Api chain handler support.
|
||||
pub mod engine;
|
||||
/// Engine orchestrator launch helper.
|
||||
pub mod launch;
|
||||
/// Metrics support.
|
||||
pub mod metrics;
|
||||
/// The background writer service, coordinating write operations on static files and the database.
|
||||
|
||||
@@ -119,7 +119,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", target = "engine::persistence", skip_all, fields(new_tip_num))]
|
||||
#[instrument(level = "debug", target = "engine::persistence", skip_all, fields(%new_tip_num))]
|
||||
fn on_remove_blocks_above(
|
||||
&self,
|
||||
new_tip_num: u64,
|
||||
|
||||
@@ -34,6 +34,10 @@ pub struct EngineApiMetrics {
|
||||
/// Metrics for EIP-7928 Block-Level Access Lists (BAL).
|
||||
#[allow(dead_code)]
|
||||
pub(crate) bal: BalMetrics,
|
||||
/// Gas-bucketed execution sub-phase metrics.
|
||||
pub(crate) execution_gas_buckets: ExecutionGasBucketMetrics,
|
||||
/// Gas-bucketed block validation sub-phase metrics.
|
||||
pub(crate) block_validation_gas_buckets: BlockValidationGasBucketMetrics,
|
||||
}
|
||||
|
||||
impl EngineApiMetrics {
|
||||
@@ -82,6 +86,22 @@ impl EngineApiMetrics {
|
||||
self.executor.post_execution_histogram.record(elapsed);
|
||||
}
|
||||
|
||||
/// Records execution duration into the gas-bucketed execution histogram.
|
||||
pub fn record_block_execution_gas_bucket(&self, gas_used: u64, elapsed: Duration) {
|
||||
let idx = GasBucketMetrics::bucket_index(gas_used);
|
||||
self.execution_gas_buckets.buckets[idx]
|
||||
.execution_gas_bucket_histogram
|
||||
.record(elapsed.as_secs_f64());
|
||||
}
|
||||
|
||||
/// Records state root duration into the gas-bucketed block validation histogram.
|
||||
pub fn record_state_root_gas_bucket(&self, gas_used: u64, elapsed_secs: f64) {
|
||||
let idx = GasBucketMetrics::bucket_index(gas_used);
|
||||
self.block_validation_gas_buckets.buckets[idx]
|
||||
.state_root_gas_bucket_histogram
|
||||
.record(elapsed_secs);
|
||||
}
|
||||
|
||||
/// Records the time spent waiting for the next transaction from the iterator.
|
||||
pub fn record_transaction_wait(&self, elapsed: Duration) {
|
||||
self.executor.transaction_wait_histogram.record(elapsed);
|
||||
@@ -280,7 +300,8 @@ impl GasBucketMetrics {
|
||||
.record(gas_used as f64 / elapsed.as_secs_f64());
|
||||
}
|
||||
|
||||
fn bucket_index(gas_used: u64) -> usize {
|
||||
/// Returns the bucket index for a given gas value.
|
||||
pub(crate) fn bucket_index(gas_used: u64) -> usize {
|
||||
GAS_BUCKET_THRESHOLDS
|
||||
.iter()
|
||||
.position(|&threshold| gas_used < threshold)
|
||||
@@ -288,7 +309,7 @@ impl GasBucketMetrics {
|
||||
}
|
||||
|
||||
/// Returns a human-readable label like `<5M`, `5-10M`, … `>40M`.
|
||||
fn bucket_label(index: usize) -> String {
|
||||
pub(crate) fn bucket_label(index: usize) -> String {
|
||||
if index == 0 {
|
||||
let hi = GAS_BUCKET_THRESHOLDS[0] / MEGAGAS;
|
||||
format!("<{hi}M")
|
||||
@@ -303,6 +324,56 @@ impl GasBucketMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-gas-bucket execution duration metric.
|
||||
#[derive(Clone, Metrics)]
|
||||
#[metrics(scope = "sync.execution")]
|
||||
pub(crate) struct ExecutionGasBucketSeries {
|
||||
/// Gas-bucketed EVM execution duration.
|
||||
pub(crate) execution_gas_bucket_histogram: Histogram,
|
||||
}
|
||||
|
||||
/// Holds pre-initialized [`ExecutionGasBucketSeries`] instances, one per gas bucket.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ExecutionGasBucketMetrics {
|
||||
buckets: [ExecutionGasBucketSeries; NUM_GAS_BUCKETS],
|
||||
}
|
||||
|
||||
impl Default for ExecutionGasBucketMetrics {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buckets: std::array::from_fn(|i| {
|
||||
let label = GasBucketMetrics::bucket_label(i);
|
||||
ExecutionGasBucketSeries::new_with_labels(&[("gas_bucket", label)])
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-gas-bucket block validation metrics (state root).
|
||||
#[derive(Clone, Metrics)]
|
||||
#[metrics(scope = "sync.block_validation")]
|
||||
pub(crate) struct BlockValidationGasBucketSeries {
|
||||
/// Gas-bucketed state root computation duration.
|
||||
pub(crate) state_root_gas_bucket_histogram: Histogram,
|
||||
}
|
||||
|
||||
/// Holds pre-initialized [`BlockValidationGasBucketSeries`] instances, one per gas bucket.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BlockValidationGasBucketMetrics {
|
||||
buckets: [BlockValidationGasBucketSeries; NUM_GAS_BUCKETS],
|
||||
}
|
||||
|
||||
impl Default for BlockValidationGasBucketMetrics {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buckets: std::array::from_fn(|i| {
|
||||
let label = GasBucketMetrics::bucket_label(i);
|
||||
BlockValidationGasBucketSeries::new_with_labels(&[("gas_bucket", label)])
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics for engine newPayload responses.
|
||||
#[derive(Metrics)]
|
||||
#[metrics(scope = "consensus.engine.beacon")]
|
||||
|
||||
@@ -2602,7 +2602,7 @@ where
|
||||
/// Returns `InsertPayloadOk::Inserted(BlockStatus::Valid)` on successful execution,
|
||||
/// `InsertPayloadOk::AlreadySeen` if the block already exists, or
|
||||
/// `InsertPayloadOk::Inserted(BlockStatus::Disconnected)` if parent state is missing.
|
||||
#[instrument(level = "debug", target = "engine::tree", skip_all, fields(block_id))]
|
||||
#[instrument(level = "debug", target = "engine::tree", skip_all, fields(?block_id))]
|
||||
fn insert_block_or_payload<Input, Err>(
|
||||
&mut self,
|
||||
block_id: BlockWithParent,
|
||||
|
||||
@@ -33,7 +33,7 @@ use reth_provider::{
|
||||
StateProviderFactory, StateReader,
|
||||
};
|
||||
use reth_revm::{db::BundleState, state::EvmState};
|
||||
use reth_tasks::Runtime;
|
||||
use reth_tasks::{ForEachOrdered, Runtime};
|
||||
use reth_trie::{hashed_cursor::HashedCursorFactory, trie_cursor::TrieCursorFactory};
|
||||
use reth_trie_parallel::{
|
||||
proof_task::{ProofTaskCtx, ProofWorkerHandle},
|
||||
@@ -43,7 +43,6 @@ use reth_trie_sparse::{
|
||||
ParallelSparseTrie, ParallelismThresholds, RevealableSparseTrie, SparseStateTrie,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
ops::Not,
|
||||
sync::{
|
||||
atomic::AtomicBool,
|
||||
@@ -97,6 +96,7 @@ pub const SPARSE_TRIE_MAX_VALUES_SHRINK_CAPACITY: usize = 1_000_000;
|
||||
/// Blocks with fewer transactions than this skip prewarming, since the fixed overhead of spawning
|
||||
/// prewarm workers exceeds the execution time saved.
|
||||
pub const SMALL_BLOCK_TX_THRESHOLD: usize = 5;
|
||||
|
||||
/// Type alias for [`PayloadHandle`] returned by payload processor spawn methods.
|
||||
type IteratorPayloadHandle<Evm, I, N> = PayloadHandle<
|
||||
WithTxEnv<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>,
|
||||
@@ -132,8 +132,6 @@ where
|
||||
/// re-use allocated memory. Stored with the block hash it was computed for to enable trie
|
||||
/// preservation across sequential payload validations.
|
||||
sparse_state_trie: SharedPreservedSparseTrie,
|
||||
/// Maximum concurrency for prewarm task.
|
||||
prewarm_max_concurrency: usize,
|
||||
/// Sparse trie prune depth.
|
||||
sparse_trie_prune_depth: usize,
|
||||
/// Maximum storage tries to retain after pruning.
|
||||
@@ -172,7 +170,6 @@ where
|
||||
precompile_cache_disabled: config.precompile_cache_disabled(),
|
||||
precompile_cache_map,
|
||||
sparse_state_trie: SharedPreservedSparseTrie::default(),
|
||||
prewarm_max_concurrency: config.prewarm_max_concurrency(),
|
||||
sparse_trie_prune_depth: config.sparse_trie_prune_depth(),
|
||||
sparse_trie_max_storage_tries: config.sparse_trie_max_storage_tries(),
|
||||
disable_sparse_trie_cache_pruning: config.disable_sparse_trie_cache_pruning(),
|
||||
@@ -248,42 +245,23 @@ where
|
||||
let (to_sparse_trie, sparse_trie_rx) = channel();
|
||||
let (to_multi_proof, from_multi_proof) = crossbeam_channel::unbounded();
|
||||
|
||||
// Extract V2 proofs flag early so we can pass it to prewarm
|
||||
let v2_proofs_enabled = !config.disable_proof_v2();
|
||||
|
||||
// Capture parent_state_root before env is moved into spawn_caching_with
|
||||
let parent_state_root = env.parent_state_root;
|
||||
let transaction_count = env.transaction_count;
|
||||
let prewarm_handle = self.spawn_caching_with(
|
||||
env,
|
||||
prewarm_rx,
|
||||
provider_builder.clone(),
|
||||
Some(to_multi_proof.clone()),
|
||||
bal,
|
||||
v2_proofs_enabled,
|
||||
);
|
||||
|
||||
// Handle BAL-based optimization if available
|
||||
let prewarm_handle = if let Some(bal) = bal {
|
||||
// When BAL is present, use BAL prewarming and send BAL to multiproof
|
||||
debug!(target: "engine::tree::payload_processor", "BAL present, using BAL prewarming");
|
||||
|
||||
// The prewarm task converts the BAL to HashedPostState and sends it on
|
||||
// to_multi_proof after slot prefetching completes.
|
||||
self.spawn_caching_with(
|
||||
env,
|
||||
prewarm_rx,
|
||||
provider_builder.clone(),
|
||||
Some(to_multi_proof.clone()),
|
||||
Some(bal),
|
||||
v2_proofs_enabled,
|
||||
)
|
||||
} else {
|
||||
// Normal path: spawn with transaction prewarming
|
||||
self.spawn_caching_with(
|
||||
env,
|
||||
prewarm_rx,
|
||||
provider_builder.clone(),
|
||||
Some(to_multi_proof.clone()),
|
||||
None,
|
||||
v2_proofs_enabled,
|
||||
)
|
||||
};
|
||||
|
||||
// Create and spawn the storage proof task
|
||||
// Create and spawn the storage proof task.
|
||||
let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
|
||||
let proof_handle = ProofWorkerHandle::new(&self.executor, task_ctx, v2_proofs_enabled);
|
||||
let halve_workers = transaction_count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD;
|
||||
let proof_handle =
|
||||
ProofWorkerHandle::new(&self.executor, task_ctx, halve_workers, v2_proofs_enabled);
|
||||
|
||||
if config.disable_trie_cache() {
|
||||
let multi_proof_task = MultiProofTask::new(
|
||||
@@ -363,6 +341,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction count threshold below which proof workers are halved, since fewer transactions
|
||||
/// produce fewer state changes and most workers would be idle overhead.
|
||||
const SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD: usize = 30;
|
||||
|
||||
/// Transaction count threshold below which sequential signature recovery is used.
|
||||
///
|
||||
/// For blocks with fewer than this many transactions, the rayon parallel iterator overhead
|
||||
@@ -374,8 +356,11 @@ where
|
||||
/// Spawns a task advancing transaction env iterator and streaming updates through a channel.
|
||||
///
|
||||
/// For blocks with fewer than [`Self::SMALL_BLOCK_TX_THRESHOLD`] transactions, uses
|
||||
/// sequential iteration to avoid rayon overhead.
|
||||
/// sequential iteration to avoid rayon overhead. For larger blocks, uses rayon parallel
|
||||
/// iteration with [`ForEachOrdered`] to recover signatures in parallel while streaming
|
||||
/// results to execution in the original transaction order.
|
||||
#[expect(clippy::type_complexity)]
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
|
||||
fn spawn_tx_iterator<I: ExecutableTxIterator<Evm>>(
|
||||
&self,
|
||||
transactions: I,
|
||||
@@ -384,9 +369,8 @@ where
|
||||
mpsc::Receiver<WithTxEnv<TxEnvFor<Evm>, I::Recovered>>,
|
||||
mpsc::Receiver<Result<WithTxEnv<TxEnvFor<Evm>, I::Recovered>, I::Error>>,
|
||||
) {
|
||||
let (ooo_tx, ooo_rx) = mpsc::channel();
|
||||
let (prewarm_tx, prewarm_rx) = mpsc::channel();
|
||||
let (execute_tx, execute_rx) = mpsc::channel();
|
||||
let (prewarm_tx, prewarm_rx) = mpsc::sync_channel(transaction_count);
|
||||
let (execute_tx, execute_rx) = mpsc::sync_channel(transaction_count);
|
||||
|
||||
if transaction_count == 0 {
|
||||
// Empty block — nothing to do.
|
||||
@@ -400,7 +384,7 @@ where
|
||||
);
|
||||
self.executor.spawn_blocking(move || {
|
||||
let (transactions, convert) = transactions.into_parts();
|
||||
for (idx, tx) in transactions.into_iter().enumerate() {
|
||||
for tx in transactions {
|
||||
let tx = convert.convert(tx);
|
||||
let tx = tx.map(|tx| {
|
||||
let (tx_env, tx) = tx.into_parts();
|
||||
@@ -409,57 +393,42 @@ where
|
||||
if let Ok(tx) = &tx {
|
||||
let _ = prewarm_tx.send(tx.clone());
|
||||
}
|
||||
let _ = ooo_tx.send((idx, tx));
|
||||
let _ = execute_tx.send(tx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Parallel path — spawn on rayon for parallel signature recovery.
|
||||
// Parallel path — recover signatures in parallel on rayon, stream results
|
||||
// to execution in order via `for_each_ordered`.
|
||||
rayon::spawn(move || {
|
||||
let (transactions, convert) = transactions.into_parts();
|
||||
transactions.into_par_iter().enumerate().for_each_with(
|
||||
ooo_tx,
|
||||
|ooo_tx, (idx, tx)| {
|
||||
transactions
|
||||
.into_par_iter()
|
||||
.map(|tx| {
|
||||
let tx = convert.convert(tx);
|
||||
let tx = tx.map(|tx| {
|
||||
tx.map(|tx| {
|
||||
let (tx_env, tx) = tx.into_parts();
|
||||
WithTxEnv { tx_env, tx: Arc::new(tx) }
|
||||
});
|
||||
// Only send Ok(_) variants to prewarming task.
|
||||
if let Ok(tx) = &tx {
|
||||
let tx = WithTxEnv { tx_env, tx: Arc::new(tx) };
|
||||
// Send to prewarming out of order — order doesn't matter there.
|
||||
let _ = prewarm_tx.send(tx.clone());
|
||||
}
|
||||
let _ = ooo_tx.send((idx, tx));
|
||||
},
|
||||
);
|
||||
tx
|
||||
})
|
||||
})
|
||||
.for_each_ordered(|tx| {
|
||||
let _ = execute_tx.send(tx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn a task that processes out-of-order transactions from the task above and sends them
|
||||
// to the execution task in order.
|
||||
self.executor.spawn_blocking(move || {
|
||||
let mut next_for_execution = 0;
|
||||
let mut queue = BTreeMap::new();
|
||||
while let Ok((idx, tx)) = ooo_rx.recv() {
|
||||
if next_for_execution == idx {
|
||||
let _ = execute_tx.send(tx);
|
||||
next_for_execution += 1;
|
||||
|
||||
while let Some(entry) = queue.first_entry() &&
|
||||
*entry.key() == next_for_execution
|
||||
{
|
||||
let _ = execute_tx.send(entry.remove());
|
||||
next_for_execution += 1;
|
||||
}
|
||||
} else {
|
||||
queue.insert(idx, tx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(prewarm_rx, execute_rx)
|
||||
}
|
||||
|
||||
/// Spawn prewarming optionally wired to the multiproof task for target updates.
|
||||
#[instrument(
|
||||
level = "debug",
|
||||
target = "engine::tree::payload_processor",
|
||||
skip_all,
|
||||
fields(bal=%bal.is_some(), %v2_proofs_enabled)
|
||||
)]
|
||||
fn spawn_caching_with<P>(
|
||||
&self,
|
||||
env: ExecutionEnv<Evm>,
|
||||
@@ -495,10 +464,8 @@ where
|
||||
self.execution_cache.clone(),
|
||||
prewarm_ctx,
|
||||
to_multi_proof,
|
||||
self.prewarm_max_concurrency,
|
||||
);
|
||||
|
||||
// spawn pre-warm task
|
||||
{
|
||||
let to_prewarm_task = to_prewarm_task.clone();
|
||||
self.executor.spawn_blocking(move || {
|
||||
|
||||
@@ -771,6 +771,11 @@ impl MultiProofTask {
|
||||
fn on_prefetch_proof(&mut self, mut targets: VersionedMultiProofTargets) -> u64 {
|
||||
// Remove already fetched proof targets to avoid redundant work.
|
||||
targets.retain_difference(&self.fetched_proof_targets);
|
||||
|
||||
if targets.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
extend_multiproof_targets(&mut self.fetched_proof_targets, &targets);
|
||||
|
||||
// For Legacy multiproofs, make sure all target accounts have an `AddedRemovedKeySet` in the
|
||||
@@ -889,6 +894,10 @@ impl MultiProofTask {
|
||||
state_updates += 1;
|
||||
}
|
||||
|
||||
if not_fetched_state_update.is_empty() {
|
||||
return state_updates;
|
||||
}
|
||||
|
||||
// Clone+Arc MultiAddedRemovedKeys for sharing with the dispatched multiproof tasks
|
||||
let multi_added_removed_keys = Arc::new(MultiAddedRemovedKeys {
|
||||
account: self.multi_added_removed_keys.account.clone(),
|
||||
@@ -1573,7 +1582,7 @@ mod tests {
|
||||
let changeset_cache = ChangesetCache::new();
|
||||
let overlay_factory = OverlayStateProviderFactory::new(factory, changeset_cache);
|
||||
let task_ctx = ProofTaskCtx::new(overlay_factory);
|
||||
let proof_handle = ProofWorkerHandle::new(runtime, task_ctx, false);
|
||||
let proof_handle = ProofWorkerHandle::new(runtime, task_ctx, false, false);
|
||||
let (to_sparse_trie, _receiver) = std::sync::mpsc::channel();
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
|
||||
@@ -2056,7 +2065,7 @@ mod tests {
|
||||
panic!("Expected PrefetchProofs message");
|
||||
};
|
||||
|
||||
assert_eq!(proofs_requested, 1);
|
||||
assert!(proofs_requested >= 1);
|
||||
}
|
||||
|
||||
/// Verifies that different message types arriving mid-batch are not lost and preserve order.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
use crate::tree::{
|
||||
cached_state::{CachedStateProvider, SavedCache},
|
||||
payload_processor::{
|
||||
bal::{self, total_slots, BALSlotIter},
|
||||
bal,
|
||||
multiproof::{MultiProofMessage, VersionedMultiProofTargets},
|
||||
PayloadExecutionCache,
|
||||
},
|
||||
@@ -25,9 +25,10 @@ use alloy_consensus::transaction::TxHashRef;
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use alloy_eips::eip4895::Withdrawal;
|
||||
use alloy_evm::Database;
|
||||
use alloy_primitives::{keccak256, map::B256Set, B256};
|
||||
use alloy_primitives::{keccak256, map::B256Set, StorageKey, B256};
|
||||
use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
|
||||
use metrics::{Counter, Gauge, Histogram};
|
||||
use rayon::prelude::*;
|
||||
use reth_evm::{execute::ExecutableTxFor, ConfigureEvm, Evm, EvmFor, RecoveredTx, SpecFor};
|
||||
use reth_metrics::Metrics;
|
||||
use reth_primitives_traits::NodePrimitives;
|
||||
@@ -39,10 +40,9 @@ use reth_revm::{database::StateProviderDatabase, state::EvmState};
|
||||
use reth_tasks::Runtime;
|
||||
use reth_trie::MultiProofTargets;
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::{self, channel, Receiver, Sender},
|
||||
mpsc::{self, channel, Receiver, Sender, SyncSender},
|
||||
Arc,
|
||||
},
|
||||
time::Instant,
|
||||
@@ -86,8 +86,6 @@ where
|
||||
execution_cache: PayloadExecutionCache,
|
||||
/// Context provided to execution tasks
|
||||
ctx: PrewarmContext<N, P, Evm>,
|
||||
/// How many transactions should be executed in parallel
|
||||
max_concurrency: usize,
|
||||
/// Sender to emit evm state outcome messages, if any.
|
||||
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
|
||||
/// Receiver for events produced by tx execution
|
||||
@@ -108,13 +106,12 @@ where
|
||||
execution_cache: PayloadExecutionCache,
|
||||
ctx: PrewarmContext<N, P, Evm>,
|
||||
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
|
||||
max_concurrency: usize,
|
||||
) -> (Self, Sender<PrewarmTaskEvent<N::Receipt>>) {
|
||||
let (actions_tx, actions_rx) = channel();
|
||||
|
||||
trace!(
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
max_concurrency,
|
||||
prewarming_threads = executor.prewarming_pool().current_num_threads(),
|
||||
transaction_count = ctx.env.transaction_count,
|
||||
"Initialized prewarm task"
|
||||
);
|
||||
@@ -124,7 +121,6 @@ where
|
||||
executor,
|
||||
execution_cache,
|
||||
ctx,
|
||||
max_concurrency,
|
||||
to_multi_proof,
|
||||
actions_rx,
|
||||
parent_span: Span::current(),
|
||||
@@ -148,24 +144,22 @@ where
|
||||
{
|
||||
let executor = self.executor.clone();
|
||||
let ctx = self.ctx.clone();
|
||||
let max_concurrency = self.max_concurrency;
|
||||
let span = Span::current();
|
||||
|
||||
self.executor.spawn_blocking(move || {
|
||||
let _enter = debug_span!(target: "engine::tree::payload_processor::prewarm", parent: span, "spawn_all").entered();
|
||||
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
|
||||
// When transaction_count is 0, it means the count is unknown. In this case, spawn
|
||||
// max workers to handle potentially many transactions in parallel rather
|
||||
// than bottlenecking on a single worker.
|
||||
let transaction_count = ctx.env.transaction_count;
|
||||
let workers_needed = if transaction_count == 0 {
|
||||
max_concurrency
|
||||
let pool_threads = executor.prewarming_pool().current_num_threads();
|
||||
// Don't spawn more workers than transactions. When transaction_count is 0
|
||||
// (unknown), use all pool threads.
|
||||
let workers_needed = if ctx.env.transaction_count > 0 {
|
||||
ctx.env.transaction_count.min(pool_threads)
|
||||
} else {
|
||||
transaction_count.min(max_concurrency)
|
||||
pool_threads
|
||||
};
|
||||
|
||||
let (done_tx, done_rx) = mpsc::sync_channel(workers_needed);
|
||||
|
||||
// Spawn workers
|
||||
let tx_sender = ctx.clone().spawn_workers(workers_needed, &executor, to_multi_proof.clone(), done_tx.clone());
|
||||
|
||||
@@ -274,10 +268,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs BAL-based prewarming by spawning workers to prefetch storage slots.
|
||||
///
|
||||
/// Divides the total slots across `max_concurrency` workers, each responsible for
|
||||
/// prefetching a range of slots from the BAL.
|
||||
/// Runs BAL-based prewarming by using the prewarming pool's parallel iterator to prefetch
|
||||
/// accounts and storage slots.
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
|
||||
fn run_bal_prewarm(
|
||||
&self,
|
||||
@@ -296,59 +288,35 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
let total_slots = total_slots(&bal);
|
||||
|
||||
trace!(
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
total_slots,
|
||||
max_concurrency = self.max_concurrency,
|
||||
"Starting BAL prewarm"
|
||||
);
|
||||
|
||||
if total_slots == 0 {
|
||||
if bal.is_empty() {
|
||||
self.send_bal_hashed_state(&bal);
|
||||
let _ =
|
||||
actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
trace!(
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
accounts = bal.len(),
|
||||
"Starting BAL prewarm"
|
||||
);
|
||||
|
||||
// Calculate number of workers needed (at most max_concurrency)
|
||||
let workers_needed = total_slots.min(self.max_concurrency);
|
||||
|
||||
// Calculate slots per worker
|
||||
let slots_per_worker = total_slots / workers_needed;
|
||||
let remainder = total_slots % workers_needed;
|
||||
|
||||
// Spawn workers with their assigned ranges
|
||||
for i in 0..workers_needed {
|
||||
let start = i * slots_per_worker + i.min(remainder);
|
||||
let extra = if i < remainder { 1 } else { 0 };
|
||||
let end = start + slots_per_worker + extra;
|
||||
|
||||
self.ctx.spawn_bal_worker(
|
||||
i,
|
||||
&self.executor,
|
||||
Arc::clone(&bal),
|
||||
start..end,
|
||||
done_tx.clone(),
|
||||
let ctx = self.ctx.clone();
|
||||
self.executor.prewarming_pool().install(|| {
|
||||
bal.par_iter().for_each_init(
|
||||
|| (ctx.clone(), None::<CachedStateProvider<reth_provider::StateProviderBox>>),
|
||||
|(ctx, provider), account| {
|
||||
if ctx.terminate_execution.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
ctx.prefetch_bal_account(provider, account);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Drop our handle to done_tx so we can detect completion
|
||||
drop(done_tx);
|
||||
|
||||
// Wait for all workers to complete
|
||||
let mut completed_workers = 0;
|
||||
while done_rx.recv().is_ok() {
|
||||
completed_workers += 1;
|
||||
}
|
||||
});
|
||||
|
||||
trace!(
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
completed_workers,
|
||||
"All BAL prewarm workers completed"
|
||||
"All BAL prewarm accounts completed"
|
||||
);
|
||||
|
||||
// Convert BAL to HashedPostState and send to multiproof task
|
||||
@@ -585,7 +553,7 @@ where
|
||||
self,
|
||||
txs: CrossbeamReceiver<IndexedTransaction<Tx>>,
|
||||
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
|
||||
done_tx: Sender<()>,
|
||||
done_tx: SyncSender<()>,
|
||||
) where
|
||||
Tx: ExecutableTxFor<Evm>,
|
||||
{
|
||||
@@ -660,7 +628,7 @@ where
|
||||
workers_needed: usize,
|
||||
task_executor: &Runtime,
|
||||
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
|
||||
done_tx: Sender<()>,
|
||||
done_tx: SyncSender<()>,
|
||||
) -> CrossbeamSender<IndexedTransaction<Tx>>
|
||||
where
|
||||
Tx: ExecutableTxFor<Evm> + Send + 'static,
|
||||
@@ -668,115 +636,65 @@ where
|
||||
let (tx_sender, tx_receiver) = crossbeam_channel::unbounded();
|
||||
|
||||
// Spawn workers that all pull from the shared receiver
|
||||
let executor = task_executor.clone();
|
||||
let span = Span::current();
|
||||
task_executor.spawn_blocking(move || {
|
||||
let _enter = span.entered();
|
||||
for idx in 0..workers_needed {
|
||||
let ctx = self.clone();
|
||||
let to_multi_proof = to_multi_proof.clone();
|
||||
let done_tx = done_tx.clone();
|
||||
let rx = tx_receiver.clone();
|
||||
let span = debug_span!(target: "engine::tree::payload_processor::prewarm", "prewarm worker", idx);
|
||||
executor.spawn_blocking(move || {
|
||||
let _enter = span.entered();
|
||||
ctx.transact_batch(rx, to_multi_proof, done_tx);
|
||||
});
|
||||
}
|
||||
});
|
||||
for idx in 0..workers_needed {
|
||||
let ctx = self.clone();
|
||||
let to_multi_proof = to_multi_proof.clone();
|
||||
let done_tx = done_tx.clone();
|
||||
let rx = tx_receiver.clone();
|
||||
let span = debug_span!(target: "engine::tree::payload_processor::prewarm", parent: &span, "prewarm worker", idx);
|
||||
task_executor.prewarming_pool().spawn(move || {
|
||||
let _enter = span.entered();
|
||||
ctx.transact_batch(rx, to_multi_proof, done_tx);
|
||||
});
|
||||
}
|
||||
|
||||
tx_sender
|
||||
}
|
||||
|
||||
/// Spawns a worker task for BAL slot prefetching.
|
||||
/// Prefetches a single account and all its storage slots from the BAL into the cache.
|
||||
///
|
||||
/// The worker iterates over the specified range of slots in the BAL and ensures
|
||||
/// each slot is loaded into the cache by accessing it through the state provider.
|
||||
fn spawn_bal_worker(
|
||||
/// The `provider` is lazily initialized on first call and reused across accounts on the same
|
||||
/// thread.
|
||||
fn prefetch_bal_account(
|
||||
&self,
|
||||
idx: usize,
|
||||
executor: &Runtime,
|
||||
bal: Arc<BlockAccessList>,
|
||||
range: Range<usize>,
|
||||
done_tx: Sender<()>,
|
||||
provider: &mut Option<CachedStateProvider<reth_provider::StateProviderBox>>,
|
||||
account: &alloy_eip7928::AccountChanges,
|
||||
) {
|
||||
let ctx = self.clone();
|
||||
let span = debug_span!(
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
"bal prewarm worker",
|
||||
idx,
|
||||
range_start = range.start,
|
||||
range_end = range.end
|
||||
);
|
||||
|
||||
executor.spawn_blocking(move || {
|
||||
let _enter = span.entered();
|
||||
ctx.prefetch_bal_slots(bal, range, done_tx);
|
||||
});
|
||||
}
|
||||
|
||||
/// Prefetches storage slots from a BAL range into the cache.
|
||||
///
|
||||
/// This iterates through the specified range of slots and accesses them via the state
|
||||
/// provider to populate the cache.
|
||||
#[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
|
||||
fn prefetch_bal_slots(
|
||||
self,
|
||||
bal: Arc<BlockAccessList>,
|
||||
range: Range<usize>,
|
||||
done_tx: Sender<()>,
|
||||
) {
|
||||
let Self { saved_cache, provider, metrics, .. } = self;
|
||||
|
||||
// Build state provider
|
||||
let state_provider = match provider.build() {
|
||||
Ok(provider) => provider,
|
||||
Err(err) => {
|
||||
trace!(
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
%err,
|
||||
"Failed to build state provider in BAL prewarm thread"
|
||||
);
|
||||
let _ = done_tx.send(());
|
||||
return;
|
||||
let state_provider = match provider {
|
||||
Some(p) => p,
|
||||
slot @ None => {
|
||||
let built = match self.provider.build() {
|
||||
Ok(p) => p,
|
||||
Err(err) => {
|
||||
trace!(
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
%err,
|
||||
"Failed to build state provider in BAL prewarm thread"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let saved_cache =
|
||||
self.saved_cache.as_ref().expect("BAL prewarm should only run with cache");
|
||||
let caches = saved_cache.cache().clone();
|
||||
let cache_metrics = saved_cache.metrics().clone();
|
||||
slot.insert(CachedStateProvider::new(built, caches, cache_metrics))
|
||||
}
|
||||
};
|
||||
|
||||
// Wrap with cache (guaranteed to be Some since run_bal_prewarm checks)
|
||||
let saved_cache = saved_cache.expect("BAL prewarm should only run with cache");
|
||||
let caches = saved_cache.cache().clone();
|
||||
let cache_metrics = saved_cache.metrics().clone();
|
||||
let state_provider = CachedStateProvider::new(state_provider, caches, cache_metrics);
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Track last seen address to avoid fetching the same account multiple times.
|
||||
let mut last_address = None;
|
||||
let _ = state_provider.basic_account(&account.address);
|
||||
|
||||
// Iterate through the assigned range of slots
|
||||
for (address, slot) in BALSlotIter::new(&bal, range.clone()) {
|
||||
// Fetch the account if this is a different address than the last one
|
||||
if last_address != Some(address) {
|
||||
let _ = state_provider.basic_account(&address);
|
||||
last_address = Some(address);
|
||||
}
|
||||
|
||||
// Access the slot to populate the cache
|
||||
let _ = state_provider.storage(address, slot);
|
||||
for slot in &account.storage_changes {
|
||||
let _ = state_provider.storage(account.address, StorageKey::from(slot.slot));
|
||||
}
|
||||
for &slot in &account.storage_reads {
|
||||
let _ = state_provider.storage(account.address, StorageKey::from(slot));
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
trace!(
|
||||
target: "engine::tree::payload_processor::prewarm",
|
||||
?range,
|
||||
elapsed_ms = elapsed.as_millis(),
|
||||
"BAL prewarm worker completed"
|
||||
);
|
||||
|
||||
// Signal completion
|
||||
let _ = done_tx.send(());
|
||||
metrics.bal_slot_iteration_duration.record(elapsed.as_secs_f64());
|
||||
self.metrics.bal_slot_iteration_duration.record(start.elapsed().as_secs_f64());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -593,18 +593,24 @@ where
|
||||
self.process_leaf_updates(true)?;
|
||||
|
||||
for (address, mut new) in self.new_storage_updates.drain() {
|
||||
let updates = self.storage_updates.entry(address).or_default();
|
||||
for (slot, new) in new.drain() {
|
||||
match updates.entry(slot) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
// Only overwrite existing entries with new values
|
||||
if new.is_changed() {
|
||||
entry.insert(new);
|
||||
match self.storage_updates.entry(address) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(new); // insert the whole map at once, no per-slot loop
|
||||
}
|
||||
Entry::Occupied(mut entry) => {
|
||||
let updates = entry.get_mut();
|
||||
for (slot, new) in new.drain() {
|
||||
match updates.entry(slot) {
|
||||
Entry::Occupied(mut slot_entry) => {
|
||||
if new.is_changed() {
|
||||
slot_entry.insert(new);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(slot_entry) => {
|
||||
slot_entry.insert(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ use std::{
|
||||
};
|
||||
use tracing::{debug, debug_span, error, info, instrument, trace, warn};
|
||||
|
||||
/// Blocks with at most this many transactions compute the state root synchronously to avoid
|
||||
/// background task overhead.
|
||||
const SMALL_BLOCK_STATE_ROOT_TX_THRESHOLD: usize = 50;
|
||||
|
||||
/// Context providing access to tree state during validation.
|
||||
///
|
||||
/// This context is provided to the [`EngineValidator`] and includes the state of the tree's
|
||||
@@ -401,7 +405,7 @@ where
|
||||
};
|
||||
|
||||
// Plan the strategy used for state root computation.
|
||||
let strategy = self.plan_state_root_computation();
|
||||
let strategy = self.plan_state_root_computation(input.transaction_count());
|
||||
|
||||
debug!(
|
||||
target: "engine::tree::payload_validator",
|
||||
@@ -597,6 +601,8 @@ where
|
||||
};
|
||||
|
||||
self.metrics.block_validation.record_state_root(&trie_output, root_elapsed.as_secs_f64());
|
||||
self.metrics
|
||||
.record_state_root_gas_bucket(block.header().gas_used(), root_elapsed.as_secs_f64());
|
||||
debug!(target: "engine::tree::payload_validator", ?root_elapsed, "Calculated state root");
|
||||
|
||||
// ensure state root matches
|
||||
@@ -765,6 +771,7 @@ where
|
||||
|
||||
let execution_duration = execution_start.elapsed();
|
||||
self.metrics.record_block_execution(&output, execution_duration);
|
||||
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))
|
||||
@@ -1142,7 +1149,7 @@ where
|
||||
level = "debug",
|
||||
target = "engine::tree::payload_validator",
|
||||
skip_all,
|
||||
fields(strategy)
|
||||
fields(?strategy)
|
||||
)]
|
||||
fn spawn_payload_processor<T: ExecutableTxIterator<Evm>>(
|
||||
&mut self,
|
||||
@@ -1237,7 +1244,12 @@ where
|
||||
///
|
||||
/// Note: Use state root task only if prefix sets are empty, otherwise proof generation is
|
||||
/// too expensive because it requires walking all paths in every proof.
|
||||
const fn plan_state_root_computation(&self) -> StateRootStrategy {
|
||||
fn plan_state_root_computation(&self, transaction_count: usize) -> StateRootStrategy {
|
||||
// Small blocks are faster without spawning parallel state root tasks.
|
||||
if transaction_count > 0 && transaction_count <= SMALL_BLOCK_STATE_ROOT_TX_THRESHOLD {
|
||||
return StateRootStrategy::Synchronous;
|
||||
}
|
||||
|
||||
if self.config.state_root_fallback() {
|
||||
StateRootStrategy::Synchronous
|
||||
} else if self.config.use_state_root_task() {
|
||||
|
||||
@@ -425,17 +425,9 @@ impl TotalDifficulty {
|
||||
|
||||
/// Convert to an [`Entry`]
|
||||
pub fn to_entry(&self) -> Entry {
|
||||
let mut data = [0u8; 32];
|
||||
|
||||
let be_bytes = self.value.to_be_bytes_vec();
|
||||
|
||||
if be_bytes.len() <= 32 {
|
||||
data[32 - be_bytes.len()..].copy_from_slice(&be_bytes);
|
||||
} else {
|
||||
data.copy_from_slice(&be_bytes[be_bytes.len() - 32..]);
|
||||
}
|
||||
|
||||
Entry::new(TOTAL_DIFFICULTY, data.to_vec())
|
||||
// era1 spec: `total-difficulty = { type: 0x0600, data: SSZ uint256 }` (little-endian)
|
||||
let data = self.value.to_le_bytes::<32>().to_vec();
|
||||
Entry::new(TOTAL_DIFFICULTY, data)
|
||||
}
|
||||
|
||||
/// Create from an [`Entry`]
|
||||
@@ -454,8 +446,8 @@ impl TotalDifficulty {
|
||||
)));
|
||||
}
|
||||
|
||||
// Convert 32-byte array to U256
|
||||
let value = U256::from_be_slice(&entry.data);
|
||||
// era1 spec: `total-difficulty = { type: 0x0600, data: SSZ uint256 }` (little-endian)
|
||||
let value = U256::from_le_slice(&entry.data);
|
||||
|
||||
Ok(Self { value })
|
||||
}
|
||||
@@ -608,6 +600,19 @@ mod tests {
|
||||
assert_eq!(recovered.value, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_difficulty_ssz_le_encoding() {
|
||||
// Verify that total-difficulty is encoded as SSZ uint256 (little-endian).
|
||||
// See https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era1.md
|
||||
let value = U256::from(1u64);
|
||||
let td = TotalDifficulty::new(value);
|
||||
let entry = td.to_entry();
|
||||
|
||||
// Little-endian: least significant byte first [1, 0, 0, ..., 0]
|
||||
assert_eq!(entry.data[0], 1, "First byte must be 1 (little-endian)");
|
||||
assert_eq!(entry.data[31], 0, "Last byte must be 0 (little-endian)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compression_roundtrip() {
|
||||
let rlp_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
@@ -158,6 +158,7 @@ where
|
||||
reserved_cpu_cores: command.engine.reserved_cpu_cores,
|
||||
proof_storage_worker_threads: command.engine.storage_worker_count,
|
||||
proof_account_worker_threads: command.engine.account_worker_count,
|
||||
prewarming_threads: command.engine.prewarming_threads,
|
||||
..Default::default()
|
||||
};
|
||||
let runner = CliRunner::try_with_runtime_config(
|
||||
|
||||
@@ -214,7 +214,7 @@ async fn blob_conversion_at_osaka() -> eyre::Result<()> {
|
||||
TransactionTestContext::validate_sidecar(envelope);
|
||||
|
||||
// build last Prague payload
|
||||
node.payload.timestamp = current_timestamp + 11;
|
||||
node.payload.timestamp = current_timestamp + 1;
|
||||
let prague_payload = node.new_payload().await?;
|
||||
assert!(matches!(prague_payload.sidecars(), BlobSidecars::Eip4844(_)));
|
||||
|
||||
@@ -227,7 +227,7 @@ async fn blob_conversion_at_osaka() -> eyre::Result<()> {
|
||||
// validate sidecar
|
||||
TransactionTestContext::validate_sidecar(envelope);
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(11)).await;
|
||||
tokio::time::sleep(Duration::from_secs(6)).await;
|
||||
|
||||
// fetch second blob tx from rpc again
|
||||
let envelope = node.rpc.envelope_by_hash(blob_tx_hash).await?;
|
||||
|
||||
@@ -282,6 +282,7 @@ async fn test_sparse_trie_reuse_across_blocks() -> eyre::Result<()> {
|
||||
.chain(MAINNET.chain)
|
||||
.genesis(serde_json::from_str(include_str!("../assets/genesis.json")).unwrap())
|
||||
.cancun_activated()
|
||||
.prague_activated()
|
||||
.build(),
|
||||
),
|
||||
false,
|
||||
|
||||
@@ -90,8 +90,8 @@ async fn test_fee_history() -> eyre::Result<()> {
|
||||
assert_eq!(block.header.gas_used, receipt.gas_used,);
|
||||
assert_eq!(block.header.base_fee_per_gas.unwrap(), expected_first_base_fee as u64);
|
||||
|
||||
for _ in 0..100 {
|
||||
let _ = GasWaster::deploy_builder(&provider, U256::from(rng.random_range(0..1000)))
|
||||
for _ in 0..20 {
|
||||
let _ = GasWaster::deploy_builder(&provider, U256::from(rng.random_range(0..100)))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -100,7 +100,7 @@ async fn test_fee_history() -> eyre::Result<()> {
|
||||
|
||||
let latest_block = provider.get_block_number().await?;
|
||||
|
||||
for _ in 0..100 {
|
||||
for _ in 0..20 {
|
||||
let latest_block = rng.random_range(0..=latest_block);
|
||||
let block_count = rng.random_range(1..=(latest_block + 1));
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use alloy_primitives::{Address, B256, U256};
|
||||
use reth_primitives_traits::{Account, Bytecode};
|
||||
use revm::database::BundleState;
|
||||
use revm::database::{states::BundleState, BundleAccount};
|
||||
|
||||
pub use alloy_evm::block::BlockExecutionResult;
|
||||
|
||||
@@ -37,6 +37,11 @@ impl<T> BlockExecutionOutput<T> {
|
||||
self.state.account(address).map(|a| a.info.as_ref().map(Into::into))
|
||||
}
|
||||
|
||||
/// Returns the state [`BundleAccount`] for the given address.
|
||||
pub fn account_state(&self, address: &Address) -> Option<&BundleAccount> {
|
||||
self.state.account(address)
|
||||
}
|
||||
|
||||
/// Get storage if value is known.
|
||||
///
|
||||
/// This means that depending on status we can potentially return `U256::ZERO`.
|
||||
|
||||
@@ -10,6 +10,7 @@ use reth_provider::{BlockReader, Chain, HeaderProvider, StateProviderFactory};
|
||||
use reth_stages_api::ExecutionStageThresholds;
|
||||
use reth_tracing::tracing::debug;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
fmt::Debug,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
@@ -286,6 +287,9 @@ where
|
||||
backfill_job: Option<StreamBackfillJob<E, P, Chain<E::Primitives>>>,
|
||||
/// Custom thresholds for the backfill job, if set.
|
||||
backfill_thresholds: Option<ExecutionStageThresholds>,
|
||||
/// Notifications that arrived during backfill and need to be delivered after it completes.
|
||||
/// These are notifications for blocks beyond the backfill range that we must not drop.
|
||||
pending_notifications: VecDeque<ExExNotification<E::Primitives>>,
|
||||
}
|
||||
|
||||
impl<P, E> ExExNotificationsWithHead<P, E>
|
||||
@@ -312,6 +316,7 @@ where
|
||||
pending_check_backfill: true,
|
||||
backfill_job: None,
|
||||
backfill_thresholds: None,
|
||||
pending_notifications: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +453,34 @@ where
|
||||
// 3. If backfill is in progress yield new notifications
|
||||
if let Some(backfill_job) = &mut this.backfill_job {
|
||||
debug!(target: "exex::notifications", "Polling backfill job");
|
||||
|
||||
// Drain the notification channel to prevent backpressure from stalling the
|
||||
// ExExManager. During backfill, the ExEx is not consuming from the channel,
|
||||
// so the capacity-1 channel fills up, which blocks the manager's PollSender,
|
||||
// which fills the manager's 1024-entry buffer, which blocks all upstream
|
||||
// senders. Notifications for blocks covered by the backfill range are
|
||||
// discarded (they'll be re-delivered by the backfill job), while
|
||||
// notifications beyond the backfill range are buffered for delivery after the
|
||||
// backfill completes.
|
||||
while let Poll::Ready(Some(notification)) = this.notifications.poll_recv(cx) {
|
||||
// Always buffer revert-containing notifications (ChainReverted,
|
||||
// ChainReorged) because the backfill job only re-delivers
|
||||
// ChainCommitted from the database. Discarding a reorg here would
|
||||
// leave the ExEx unaware of the fork switch.
|
||||
if notification.reverted_chain().is_some() {
|
||||
this.pending_notifications.push_back(notification);
|
||||
continue;
|
||||
}
|
||||
if let Some(committed) = notification.committed_chain() &&
|
||||
committed.tip().number() <= this.initial_local_head.number
|
||||
{
|
||||
// Covered by backfill range, safe to discard
|
||||
continue;
|
||||
}
|
||||
// Beyond the backfill range — buffer for delivery after backfill
|
||||
this.pending_notifications.push_back(notification);
|
||||
}
|
||||
|
||||
if let Some(chain) = ready!(backfill_job.poll_next_unpin(cx)).transpose()? {
|
||||
debug!(target: "exex::notifications", range = ?chain.range(), "Backfill job returned a chain");
|
||||
return Poll::Ready(Some(Ok(ExExNotification::ChainCommitted {
|
||||
@@ -459,13 +492,18 @@ where
|
||||
this.backfill_job = None;
|
||||
}
|
||||
|
||||
// 4. Otherwise advance the regular event stream
|
||||
// 4. Deliver any notifications that were buffered during backfill
|
||||
if let Some(notification) = this.pending_notifications.pop_front() {
|
||||
return Poll::Ready(Some(Ok(notification)))
|
||||
}
|
||||
|
||||
// 5. Otherwise advance the regular event stream
|
||||
loop {
|
||||
let Some(notification) = ready!(this.notifications.poll_recv(cx)) else {
|
||||
return Poll::Ready(None)
|
||||
};
|
||||
|
||||
// 5. In case the exex is ahead of the new tip, we must skip it
|
||||
// 6. In case the exex is ahead of the new tip, we must skip it
|
||||
if let Some(committed) = notification.committed_chain() {
|
||||
// inclusive check because we should start with `exex.head + 1`
|
||||
if this.initial_exex_head.block.number >= committed.tip().number() {
|
||||
@@ -789,4 +827,135 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/paradigmxyz/reth/issues/19665>.
|
||||
///
|
||||
/// During backfill, `poll_next` must drain the notification channel so that
|
||||
/// the upstream `ExExManager` is never blocked by a full channel. Without
|
||||
/// the drain loop the capacity-1 channel stays full for the entire backfill
|
||||
/// duration, which stalls the manager's `PollSender` and eventually blocks
|
||||
/// all upstream senders once the 1024-entry buffer fills up.
|
||||
///
|
||||
/// The key assertion is the `try_send` after the first `poll_next`: it
|
||||
/// proves the channel was drained during the backfill poll. Without the
|
||||
/// fix this `try_send` fails because the notification is still sitting in
|
||||
/// the channel.
|
||||
#[tokio::test]
|
||||
async fn exex_notifications_backfill_drains_channel() -> eyre::Result<()> {
|
||||
let mut rng = generators::rng();
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let wal = Wal::new(temp_dir.path()).unwrap();
|
||||
|
||||
let provider_factory = create_test_provider_factory();
|
||||
let genesis_hash = init_genesis(&provider_factory)?;
|
||||
let genesis_block = provider_factory
|
||||
.block(genesis_hash.into())?
|
||||
.ok_or_else(|| eyre::eyre!("genesis block not found"))?;
|
||||
|
||||
let provider = BlockchainProvider::new(provider_factory.clone())?;
|
||||
|
||||
// Insert block 1 into the DB so there's something to backfill
|
||||
let node_head_block = random_block(
|
||||
&mut rng,
|
||||
genesis_block.number + 1,
|
||||
BlockParams { parent: Some(genesis_hash), tx_count: Some(0), ..Default::default() },
|
||||
)
|
||||
.try_recover()?;
|
||||
let node_head = node_head_block.num_hash();
|
||||
let provider_rw = provider_factory.provider_rw()?;
|
||||
provider_rw.insert_block(&node_head_block)?;
|
||||
provider_rw.commit()?;
|
||||
|
||||
// ExEx head is at genesis — backfill will run for block 1
|
||||
let exex_head =
|
||||
ExExHead { block: BlockNumHash { number: genesis_block.number, hash: genesis_hash } };
|
||||
|
||||
// Notification for a block AFTER the backfill range (block 2).
|
||||
let post_backfill_notification = ExExNotification::ChainCommitted {
|
||||
new: Arc::new(Chain::new(
|
||||
vec![random_block(
|
||||
&mut rng,
|
||||
node_head.number + 1,
|
||||
BlockParams { parent: Some(node_head.hash), ..Default::default() },
|
||||
)
|
||||
.try_recover()?],
|
||||
Default::default(),
|
||||
BTreeMap::new(),
|
||||
)),
|
||||
};
|
||||
|
||||
// Another notification (block 3) used to probe channel capacity.
|
||||
let probe_notification = ExExNotification::ChainCommitted {
|
||||
new: Arc::new(Chain::new(
|
||||
vec![random_block(
|
||||
&mut rng,
|
||||
node_head.number + 2,
|
||||
BlockParams { parent: None, ..Default::default() },
|
||||
)
|
||||
.try_recover()?],
|
||||
Default::default(),
|
||||
BTreeMap::new(),
|
||||
)),
|
||||
};
|
||||
|
||||
let (notifications_tx, notifications_rx) = mpsc::channel(1);
|
||||
|
||||
// Fill the capacity-1 channel.
|
||||
notifications_tx.send(post_backfill_notification.clone()).await?;
|
||||
|
||||
// Confirm the channel is full — this is the precondition that causes the
|
||||
// stall in production: the ExExManager's PollSender would block here.
|
||||
assert!(
|
||||
notifications_tx.try_send(probe_notification.clone()).is_err(),
|
||||
"channel should be full before backfill poll"
|
||||
);
|
||||
|
||||
let mut notifications = ExExNotificationsWithoutHead::new(
|
||||
node_head,
|
||||
provider,
|
||||
EthEvmConfig::mainnet(),
|
||||
notifications_rx,
|
||||
wal.handle(),
|
||||
)
|
||||
.with_head(exex_head);
|
||||
|
||||
// Poll once — this returns the backfill result for block 1. Crucially,
|
||||
// the drain loop in poll_next runs in this same call, consuming the
|
||||
// notification from the channel and buffering it.
|
||||
let backfill_result = notifications.next().await.transpose()?;
|
||||
assert_eq!(
|
||||
backfill_result,
|
||||
Some(ExExNotification::ChainCommitted {
|
||||
new: Arc::new(
|
||||
BackfillJobFactory::new(
|
||||
notifications.evm_config.clone(),
|
||||
notifications.provider.clone()
|
||||
)
|
||||
.backfill(1..=1)
|
||||
.next()
|
||||
.ok_or_eyre("failed to backfill")??
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
// KEY ASSERTION: the channel was drained during the backfill poll above.
|
||||
// Without the drain loop this try_send fails because the original
|
||||
// notification is still occupying the capacity-1 channel.
|
||||
assert!(
|
||||
notifications_tx.try_send(probe_notification.clone()).is_ok(),
|
||||
"channel should have been drained during backfill poll"
|
||||
);
|
||||
|
||||
// The first buffered notification (block 2) was drained from the channel
|
||||
// during backfill and is delivered now.
|
||||
let buffered = notifications.next().await.transpose()?;
|
||||
assert_eq!(buffered, Some(post_backfill_notification));
|
||||
|
||||
// The probe notification (block 3) that we just sent is delivered next.
|
||||
let probe = notifications.next().await.transpose()?;
|
||||
assert_eq!(probe, Some(probe_notification));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
27
crates/net/eth-wire-types/src/block_access_lists.rs
Normal file
27
crates/net/eth-wire-types/src/block_access_lists.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
//! Implements the `GetBlockAccessLists` and `BlockAccessLists` message types.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use alloy_primitives::{Bytes, B256};
|
||||
use alloy_rlp::{RlpDecodableWrapper, RlpEncodableWrapper};
|
||||
use reth_codecs_derive::add_arbitrary_tests;
|
||||
|
||||
/// A request for block access lists from the given block hashes.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
|
||||
#[add_arbitrary_tests(rlp)]
|
||||
pub struct GetBlockAccessLists(
|
||||
/// The block hashes to request block access lists for.
|
||||
pub Vec<B256>,
|
||||
);
|
||||
|
||||
/// Response for [`GetBlockAccessLists`] containing one BAL per requested block hash.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
|
||||
#[add_arbitrary_tests(rlp)]
|
||||
pub struct BlockAccessLists(
|
||||
/// The requested block access lists as opaque bytes. Unavailable entries are represented by
|
||||
/// empty byte slices.
|
||||
pub Vec<Bytes>,
|
||||
);
|
||||
@@ -169,7 +169,10 @@ impl NewPooledTransactionHashes {
|
||||
matches!(version, EthVersion::Eth67 | EthVersion::Eth66)
|
||||
}
|
||||
Self::Eth68(_) => {
|
||||
matches!(version, EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70)
|
||||
matches!(
|
||||
version,
|
||||
EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70 | EthVersion::Eth71
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,11 @@ impl Capability {
|
||||
Self::eth(EthVersion::Eth70)
|
||||
}
|
||||
|
||||
/// Returns the [`EthVersion::Eth71`] capability.
|
||||
pub const fn eth_71() -> Self {
|
||||
Self::eth(EthVersion::Eth71)
|
||||
}
|
||||
|
||||
/// Whether this is eth v66 protocol.
|
||||
#[inline]
|
||||
pub fn is_eth_v66(&self) -> bool {
|
||||
@@ -140,6 +145,12 @@ impl Capability {
|
||||
self.name == "eth" && self.version == 70
|
||||
}
|
||||
|
||||
/// Whether this is eth v71.
|
||||
#[inline]
|
||||
pub fn is_eth_v71(&self) -> bool {
|
||||
self.name == "eth" && self.version == 71
|
||||
}
|
||||
|
||||
/// Whether this is any eth version.
|
||||
#[inline]
|
||||
pub fn is_eth(&self) -> bool {
|
||||
@@ -147,7 +158,8 @@ impl Capability {
|
||||
self.is_eth_v67() ||
|
||||
self.is_eth_v68() ||
|
||||
self.is_eth_v69() ||
|
||||
self.is_eth_v70()
|
||||
self.is_eth_v70() ||
|
||||
self.is_eth_v71()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +179,7 @@ impl From<EthVersion> for Capability {
|
||||
#[cfg(any(test, feature = "arbitrary"))]
|
||||
impl<'a> arbitrary::Arbitrary<'a> for Capability {
|
||||
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||
let version = u.int_in_range(66..=70)?; // Valid eth protocol versions are 66-70
|
||||
let version = u.int_in_range(66..=71)?; // Valid eth protocol versions are 66-71
|
||||
// Only generate valid eth protocol name for now since it's the only supported protocol
|
||||
Ok(Self::new_static("eth", version))
|
||||
}
|
||||
@@ -183,6 +195,7 @@ pub struct Capabilities {
|
||||
eth_68: bool,
|
||||
eth_69: bool,
|
||||
eth_70: bool,
|
||||
eth_71: bool,
|
||||
}
|
||||
|
||||
impl Capabilities {
|
||||
@@ -194,6 +207,7 @@ impl Capabilities {
|
||||
eth_68: value.iter().any(Capability::is_eth_v68),
|
||||
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),
|
||||
inner: value,
|
||||
}
|
||||
}
|
||||
@@ -212,7 +226,7 @@ impl Capabilities {
|
||||
/// Whether the peer supports `eth` sub-protocol.
|
||||
#[inline]
|
||||
pub const fn supports_eth(&self) -> bool {
|
||||
self.eth_70 || self.eth_69 || self.eth_68 || self.eth_67 || self.eth_66
|
||||
self.eth_71 || self.eth_70 || self.eth_69 || self.eth_68 || self.eth_67 || self.eth_66
|
||||
}
|
||||
|
||||
/// Whether this peer supports eth v66 protocol.
|
||||
@@ -244,6 +258,12 @@ impl Capabilities {
|
||||
pub const fn supports_eth_v70(&self) -> bool {
|
||||
self.eth_70
|
||||
}
|
||||
|
||||
/// Whether this peer supports eth v71 protocol.
|
||||
#[inline]
|
||||
pub const fn supports_eth_v71(&self) -> bool {
|
||||
self.eth_71
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Capability>> for Capabilities {
|
||||
@@ -268,6 +288,7 @@ impl Decodable for Capabilities {
|
||||
eth_68: inner.iter().any(Capability::is_eth_v68),
|
||||
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),
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ pub use state::*;
|
||||
pub mod receipts;
|
||||
pub use receipts::*;
|
||||
|
||||
pub mod block_access_lists;
|
||||
pub use block_access_lists::*;
|
||||
|
||||
pub mod disconnect_reason;
|
||||
pub use disconnect_reason::*;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Implements Ethereum wire protocol for versions 66 through 70.
|
||||
//! Implements Ethereum wire protocol for versions 66 through 71.
|
||||
//! Defines structs/enums for messages, request-response pairs, and broadcasts.
|
||||
//! Handles compatibility with [`EthVersion`].
|
||||
//!
|
||||
@@ -7,10 +7,10 @@
|
||||
//! Reference: [Ethereum Wire Protocol](https://github.com/ethereum/devp2p/blob/master/caps/eth.md).
|
||||
|
||||
use super::{
|
||||
broadcast::NewBlockHashes, BlockBodies, BlockHeaders, GetBlockBodies, GetBlockHeaders,
|
||||
GetNodeData, GetPooledTransactions, GetReceipts, GetReceipts70, NewPooledTransactionHashes66,
|
||||
NewPooledTransactionHashes68, NodeData, PooledTransactions, Receipts, Status, StatusEth69,
|
||||
Transactions,
|
||||
broadcast::NewBlockHashes, BlockAccessLists, BlockBodies, BlockHeaders, GetBlockAccessLists,
|
||||
GetBlockBodies, GetBlockHeaders, GetNodeData, GetPooledTransactions, GetReceipts,
|
||||
GetReceipts70, NewPooledTransactionHashes66, NewPooledTransactionHashes68, NodeData,
|
||||
PooledTransactions, Receipts, Status, StatusEth69, Transactions,
|
||||
};
|
||||
use crate::{
|
||||
status::StatusMessage, BlockRangeUpdate, EthNetworkPrimitives, EthVersion, NetworkPrimitives,
|
||||
@@ -168,6 +168,18 @@ impl<N: NetworkPrimitives> ProtocolMessage<N> {
|
||||
}
|
||||
EthMessage::BlockRangeUpdate(BlockRangeUpdate::decode(buf)?)
|
||||
}
|
||||
EthMessageID::GetBlockAccessLists => {
|
||||
if version < EthVersion::Eth71 {
|
||||
return Err(MessageError::Invalid(version, EthMessageID::GetBlockAccessLists))
|
||||
}
|
||||
EthMessage::GetBlockAccessLists(RequestPair::decode(buf)?)
|
||||
}
|
||||
EthMessageID::BlockAccessLists => {
|
||||
if version < EthVersion::Eth71 {
|
||||
return Err(MessageError::Invalid(version, EthMessageID::BlockAccessLists))
|
||||
}
|
||||
EthMessage::BlockAccessLists(RequestPair::decode(buf)?)
|
||||
}
|
||||
EthMessageID::Other(_) => {
|
||||
let raw_payload = Bytes::copy_from_slice(buf);
|
||||
buf.advance(raw_payload.len());
|
||||
@@ -250,6 +262,8 @@ impl<N: NetworkPrimitives> From<EthBroadcastMessage<N>> for ProtocolBroadcastMes
|
||||
///
|
||||
/// The `eth/70` (EIP-7975) keeps the eth/69 status format and introduces partial receipts.
|
||||
/// requests/responses.
|
||||
///
|
||||
/// The `eth/71` draft extends eth/70 with block access list request/response messages.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum EthMessage<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
@@ -310,6 +324,8 @@ pub enum EthMessage<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// `GetReceipts` in EIP-7975 inlines the request id. The type still wraps
|
||||
/// a [`RequestPair`], but with a custom inline encoding.
|
||||
GetReceipts70(RequestPair<GetReceipts70>),
|
||||
/// Represents a `GetBlockAccessLists` request-response pair for eth/71.
|
||||
GetBlockAccessLists(RequestPair<GetBlockAccessLists>),
|
||||
/// Represents a Receipts request-response pair.
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
@@ -332,6 +348,8 @@ pub enum EthMessage<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// request id. The type still wraps a [`RequestPair`], but with a custom
|
||||
/// inline encoding.
|
||||
Receipts70(RequestPair<Receipts70<N::Receipt>>),
|
||||
/// Represents a `BlockAccessLists` request-response pair for eth/71.
|
||||
BlockAccessLists(RequestPair<BlockAccessLists>),
|
||||
/// Represents a `BlockRangeUpdate` message broadcast to the network.
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
@@ -364,6 +382,8 @@ impl<N: NetworkPrimitives> EthMessage<N> {
|
||||
Self::GetReceipts(_) | Self::GetReceipts70(_) => EthMessageID::GetReceipts,
|
||||
Self::Receipts(_) | Self::Receipts69(_) | Self::Receipts70(_) => EthMessageID::Receipts,
|
||||
Self::BlockRangeUpdate(_) => EthMessageID::BlockRangeUpdate,
|
||||
Self::GetBlockAccessLists(_) => EthMessageID::GetBlockAccessLists,
|
||||
Self::BlockAccessLists(_) => EthMessageID::BlockAccessLists,
|
||||
Self::Other(msg) => EthMessageID::Other(msg.id as u8),
|
||||
}
|
||||
}
|
||||
@@ -376,6 +396,7 @@ impl<N: NetworkPrimitives> EthMessage<N> {
|
||||
Self::GetBlockHeaders(_) |
|
||||
Self::GetReceipts(_) |
|
||||
Self::GetReceipts70(_) |
|
||||
Self::GetBlockAccessLists(_) |
|
||||
Self::GetPooledTransactions(_) |
|
||||
Self::GetNodeData(_)
|
||||
)
|
||||
@@ -389,6 +410,7 @@ impl<N: NetworkPrimitives> EthMessage<N> {
|
||||
Self::Receipts(_) |
|
||||
Self::Receipts69(_) |
|
||||
Self::Receipts70(_) |
|
||||
Self::BlockAccessLists(_) |
|
||||
Self::BlockHeaders(_) |
|
||||
Self::BlockBodies(_) |
|
||||
Self::NodeData(_)
|
||||
@@ -443,9 +465,11 @@ impl<N: NetworkPrimitives> Encodable for EthMessage<N> {
|
||||
Self::NodeData(data) => data.encode(out),
|
||||
Self::GetReceipts(request) => request.encode(out),
|
||||
Self::GetReceipts70(request) => request.encode(out),
|
||||
Self::GetBlockAccessLists(request) => request.encode(out),
|
||||
Self::Receipts(receipts) => receipts.encode(out),
|
||||
Self::Receipts69(receipt69) => receipt69.encode(out),
|
||||
Self::Receipts70(receipt70) => receipt70.encode(out),
|
||||
Self::BlockAccessLists(block_access_lists) => block_access_lists.encode(out),
|
||||
Self::BlockRangeUpdate(block_range_update) => block_range_update.encode(out),
|
||||
Self::Other(unknown) => out.put_slice(&unknown.payload),
|
||||
}
|
||||
@@ -468,9 +492,11 @@ impl<N: NetworkPrimitives> Encodable for EthMessage<N> {
|
||||
Self::NodeData(data) => data.length(),
|
||||
Self::GetReceipts(request) => request.length(),
|
||||
Self::GetReceipts70(request) => request.length(),
|
||||
Self::GetBlockAccessLists(request) => request.length(),
|
||||
Self::Receipts(receipts) => receipts.length(),
|
||||
Self::Receipts69(receipt69) => receipt69.length(),
|
||||
Self::Receipts70(receipt70) => receipt70.length(),
|
||||
Self::BlockAccessLists(block_access_lists) => block_access_lists.length(),
|
||||
Self::BlockRangeUpdate(block_range_update) => block_range_update.length(),
|
||||
Self::Other(unknown) => unknown.length(),
|
||||
}
|
||||
@@ -559,6 +585,14 @@ pub enum EthMessageID {
|
||||
///
|
||||
/// Introduced in Eth69
|
||||
BlockRangeUpdate = 0x11,
|
||||
/// Requests block access lists.
|
||||
///
|
||||
/// Introduced in Eth71
|
||||
GetBlockAccessLists = 0x12,
|
||||
/// Represents block access lists.
|
||||
///
|
||||
/// Introduced in Eth71
|
||||
BlockAccessLists = 0x13,
|
||||
/// Represents unknown message types.
|
||||
Other(u8),
|
||||
}
|
||||
@@ -583,13 +617,17 @@ impl EthMessageID {
|
||||
Self::GetReceipts => 0x0f,
|
||||
Self::Receipts => 0x10,
|
||||
Self::BlockRangeUpdate => 0x11,
|
||||
Self::GetBlockAccessLists => 0x12,
|
||||
Self::BlockAccessLists => 0x13,
|
||||
Self::Other(value) => *value, // Return the stored `u8`
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the max value for the given version.
|
||||
pub const fn max(version: EthVersion) -> u8 {
|
||||
if version as u8 >= EthVersion::Eth69 as u8 {
|
||||
if version.is_eth71() {
|
||||
Self::BlockAccessLists.to_u8()
|
||||
} else if version.is_eth69_or_newer() {
|
||||
Self::BlockRangeUpdate.to_u8()
|
||||
} else {
|
||||
Self::Receipts.to_u8()
|
||||
@@ -634,6 +672,8 @@ impl Decodable for EthMessageID {
|
||||
0x0f => Self::GetReceipts,
|
||||
0x10 => Self::Receipts,
|
||||
0x11 => Self::BlockRangeUpdate,
|
||||
0x12 => Self::GetBlockAccessLists,
|
||||
0x13 => Self::BlockAccessLists,
|
||||
unknown => Self::Other(*unknown),
|
||||
};
|
||||
buf.advance(1);
|
||||
@@ -662,6 +702,8 @@ impl TryFrom<usize> for EthMessageID {
|
||||
0x0f => Ok(Self::GetReceipts),
|
||||
0x10 => Ok(Self::Receipts),
|
||||
0x11 => Ok(Self::BlockRangeUpdate),
|
||||
0x12 => Ok(Self::GetBlockAccessLists),
|
||||
0x13 => Ok(Self::BlockAccessLists),
|
||||
_ => Err("Invalid message ID"),
|
||||
}
|
||||
}
|
||||
@@ -742,8 +784,9 @@ where
|
||||
mod tests {
|
||||
use super::MessageError;
|
||||
use crate::{
|
||||
message::RequestPair, EthMessage, EthMessageID, EthNetworkPrimitives, EthVersion,
|
||||
GetNodeData, NodeData, ProtocolMessage, RawCapabilityMessage,
|
||||
message::RequestPair, BlockAccessLists, EthMessage, EthMessageID, EthNetworkPrimitives,
|
||||
EthVersion, GetBlockAccessLists, GetNodeData, NodeData, ProtocolMessage,
|
||||
RawCapabilityMessage,
|
||||
};
|
||||
use alloy_primitives::hex;
|
||||
use alloy_rlp::{Decodable, Encodable, Error};
|
||||
@@ -784,6 +827,60 @@ mod tests {
|
||||
assert!(matches!(msg, Err(MessageError::Invalid(..))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bal_message_version_gating() {
|
||||
let get_block_access_lists =
|
||||
EthMessage::<EthNetworkPrimitives>::GetBlockAccessLists(RequestPair {
|
||||
request_id: 1337,
|
||||
message: GetBlockAccessLists(vec![]),
|
||||
});
|
||||
let buf = encode(ProtocolMessage {
|
||||
message_type: EthMessageID::GetBlockAccessLists,
|
||||
message: get_block_access_lists,
|
||||
});
|
||||
let msg = ProtocolMessage::<EthNetworkPrimitives>::decode_message(
|
||||
EthVersion::Eth70,
|
||||
&mut &buf[..],
|
||||
);
|
||||
assert!(matches!(
|
||||
msg,
|
||||
Err(MessageError::Invalid(EthVersion::Eth70, EthMessageID::GetBlockAccessLists))
|
||||
));
|
||||
|
||||
let block_access_lists =
|
||||
EthMessage::<EthNetworkPrimitives>::BlockAccessLists(RequestPair {
|
||||
request_id: 1337,
|
||||
message: BlockAccessLists(vec![]),
|
||||
});
|
||||
let buf = encode(ProtocolMessage {
|
||||
message_type: EthMessageID::BlockAccessLists,
|
||||
message: block_access_lists,
|
||||
});
|
||||
let msg = ProtocolMessage::<EthNetworkPrimitives>::decode_message(
|
||||
EthVersion::Eth70,
|
||||
&mut &buf[..],
|
||||
);
|
||||
assert!(matches!(
|
||||
msg,
|
||||
Err(MessageError::Invalid(EthVersion::Eth70, EthMessageID::BlockAccessLists))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bal_message_eth71_roundtrip() {
|
||||
let msg = ProtocolMessage::from(EthMessage::<EthNetworkPrimitives>::GetBlockAccessLists(
|
||||
RequestPair { request_id: 42, message: GetBlockAccessLists(vec![]) },
|
||||
));
|
||||
let encoded = encode(msg.clone());
|
||||
let decoded = ProtocolMessage::<EthNetworkPrimitives>::decode_message(
|
||||
EthVersion::Eth71,
|
||||
&mut &encoded[..],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(decoded, msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_pair_encode() {
|
||||
let request_pair = RequestPair { request_id: 1337, message: vec![5u8] };
|
||||
|
||||
@@ -29,6 +29,8 @@ pub enum EthVersion {
|
||||
Eth69 = 69,
|
||||
/// The `eth` protocol version 70.
|
||||
Eth70 = 70,
|
||||
/// The `eth` protocol version 71.
|
||||
Eth71 = 71,
|
||||
}
|
||||
|
||||
impl EthVersion {
|
||||
@@ -62,9 +64,19 @@ impl EthVersion {
|
||||
pub const fn is_eth70(&self) -> bool {
|
||||
matches!(self, Self::Eth70)
|
||||
}
|
||||
|
||||
/// Returns true if the version is eth/71
|
||||
pub const fn is_eth71(&self) -> bool {
|
||||
matches!(self, Self::Eth71)
|
||||
}
|
||||
|
||||
/// Returns true if the version is eth/69 or newer.
|
||||
pub const fn is_eth69_or_newer(&self) -> bool {
|
||||
matches!(self, Self::Eth69 | Self::Eth70 | Self::Eth71)
|
||||
}
|
||||
}
|
||||
|
||||
/// RLP encodes `EthVersion` as a single byte (66-69).
|
||||
/// RLP encodes `EthVersion` as a single byte (66-71).
|
||||
impl Encodable for EthVersion {
|
||||
fn encode(&self, out: &mut dyn BufMut) {
|
||||
(*self as u8).encode(out)
|
||||
@@ -76,7 +88,7 @@ impl Encodable for EthVersion {
|
||||
}
|
||||
|
||||
/// RLP decodes a single byte into `EthVersion`.
|
||||
/// Returns error if byte is not a valid version (66-69).
|
||||
/// Returns error if byte is not a valid version (66-71).
|
||||
impl Decodable for EthVersion {
|
||||
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
|
||||
let version = u8::decode(buf)?;
|
||||
@@ -104,6 +116,7 @@ impl TryFrom<&str> for EthVersion {
|
||||
"68" => Ok(Self::Eth68),
|
||||
"69" => Ok(Self::Eth69),
|
||||
"70" => Ok(Self::Eth70),
|
||||
"71" => Ok(Self::Eth71),
|
||||
_ => Err(ParseVersionError(s.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -129,6 +142,7 @@ impl TryFrom<u8> for EthVersion {
|
||||
68 => Ok(Self::Eth68),
|
||||
69 => Ok(Self::Eth69),
|
||||
70 => Ok(Self::Eth70),
|
||||
71 => Ok(Self::Eth71),
|
||||
_ => Err(ParseVersionError(u.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -159,6 +173,7 @@ impl From<EthVersion> for &'static str {
|
||||
EthVersion::Eth68 => "68",
|
||||
EthVersion::Eth69 => "69",
|
||||
EthVersion::Eth70 => "70",
|
||||
EthVersion::Eth71 => "71",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +231,7 @@ mod tests {
|
||||
assert_eq!(EthVersion::Eth68, EthVersion::try_from("68").unwrap());
|
||||
assert_eq!(EthVersion::Eth69, EthVersion::try_from("69").unwrap());
|
||||
assert_eq!(EthVersion::Eth70, EthVersion::try_from("70").unwrap());
|
||||
assert_eq!(EthVersion::Eth71, EthVersion::try_from("71").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -225,6 +241,7 @@ mod tests {
|
||||
assert_eq!(EthVersion::Eth68, "68".parse().unwrap());
|
||||
assert_eq!(EthVersion::Eth69, "69".parse().unwrap());
|
||||
assert_eq!(EthVersion::Eth70, "70".parse().unwrap());
|
||||
assert_eq!(EthVersion::Eth71, "71".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -235,6 +252,7 @@ mod tests {
|
||||
EthVersion::Eth68,
|
||||
EthVersion::Eth69,
|
||||
EthVersion::Eth70,
|
||||
EthVersion::Eth71,
|
||||
];
|
||||
|
||||
for version in versions {
|
||||
@@ -253,6 +271,7 @@ mod tests {
|
||||
(68_u8, Ok(EthVersion::Eth68)),
|
||||
(69_u8, Ok(EthVersion::Eth69)),
|
||||
(70_u8, Ok(EthVersion::Eth70)),
|
||||
(71_u8, Ok(EthVersion::Eth71)),
|
||||
(65_u8, Err(RlpError::Custom("invalid eth version"))),
|
||||
];
|
||||
|
||||
|
||||
@@ -294,7 +294,8 @@ mod tests {
|
||||
use alloy_primitives::B256;
|
||||
use alloy_rlp::Encodable;
|
||||
use reth_eth_wire_types::{
|
||||
message::RequestPair, GetAccountRangeMessage, GetBlockHeaders, HeadersDirection,
|
||||
message::RequestPair, GetAccountRangeMessage, GetBlockAccessLists, GetBlockHeaders,
|
||||
HeadersDirection,
|
||||
};
|
||||
|
||||
// Helper to create eth message and its bytes
|
||||
@@ -419,4 +420,40 @@ mod tests {
|
||||
let snap_boundary_result = inner.decode_message(snap_boundary_bytes);
|
||||
assert!(snap_boundary_result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eth70_message_id_0x12_is_snap() {
|
||||
let inner = EthSnapStreamInner::<EthNetworkPrimitives>::new(EthVersion::Eth70);
|
||||
let snap_msg = SnapProtocolMessage::GetAccountRange(GetAccountRangeMessage {
|
||||
request_id: 1,
|
||||
root_hash: B256::default(),
|
||||
starting_hash: B256::default(),
|
||||
limit_hash: B256::default(),
|
||||
response_bytes: 1000,
|
||||
});
|
||||
|
||||
let encoded = inner.encode_snap_message(snap_msg);
|
||||
assert_eq!(encoded[0], EthMessageID::message_count(EthVersion::Eth70));
|
||||
|
||||
let decoded = inner.decode_message(BytesMut::from(&encoded[..])).unwrap();
|
||||
assert!(matches!(decoded, EthSnapMessage::Snap(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eth71_message_id_0x12_is_eth() {
|
||||
let inner = EthSnapStreamInner::<EthNetworkPrimitives>::new(EthVersion::Eth71);
|
||||
let eth_msg = EthMessage::<EthNetworkPrimitives>::GetBlockAccessLists(RequestPair {
|
||||
request_id: 1,
|
||||
message: GetBlockAccessLists(vec![B256::ZERO]),
|
||||
});
|
||||
let protocol_msg = ProtocolMessage::from(eth_msg.clone());
|
||||
let mut buf = Vec::new();
|
||||
protocol_msg.encode(&mut buf);
|
||||
|
||||
let decoded = inner.decode_message(BytesMut::from(&buf[..])).unwrap();
|
||||
let EthSnapMessage::Eth(decoded_eth) = decoded else {
|
||||
panic!("expected eth message");
|
||||
};
|
||||
assert_eq!(decoded_eth, eth_msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,5 +84,7 @@ mod tests {
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth67).messages(), 17);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth68).messages(), 17);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth69).messages(), 18);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth70).messages(), 18);
|
||||
assert_eq!(Protocol::eth(EthVersion::Eth71).messages(), 20);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! API related to listening for network events.
|
||||
|
||||
use reth_eth_wire_types::{
|
||||
message::RequestPair, BlockBodies, BlockHeaders, Capabilities, DisconnectReason, EthMessage,
|
||||
EthNetworkPrimitives, EthVersion, GetBlockBodies, GetBlockHeaders, GetNodeData,
|
||||
GetPooledTransactions, GetReceipts, GetReceipts70, NetworkPrimitives, NodeData,
|
||||
PooledTransactions, Receipts, Receipts69, Receipts70, UnifiedStatus,
|
||||
message::RequestPair, BlockAccessLists, BlockBodies, BlockHeaders, Capabilities,
|
||||
DisconnectReason, EthMessage, EthNetworkPrimitives, EthVersion, GetBlockAccessLists,
|
||||
GetBlockBodies, GetBlockHeaders, GetNodeData, GetPooledTransactions, GetReceipts,
|
||||
GetReceipts70, NetworkPrimitives, NodeData, PooledTransactions, Receipts, Receipts69,
|
||||
Receipts70, UnifiedStatus,
|
||||
};
|
||||
use reth_ethereum_forks::ForkId;
|
||||
use reth_network_p2p::error::{RequestError, RequestResult};
|
||||
@@ -252,6 +253,15 @@ pub enum PeerRequest<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// The channel to send the response for receipts.
|
||||
response: oneshot::Sender<RequestResult<Receipts70<N::Receipt>>>,
|
||||
},
|
||||
/// Requests block access lists from the peer.
|
||||
///
|
||||
/// The response should be sent through the channel.
|
||||
GetBlockAccessLists {
|
||||
/// The request for block access lists.
|
||||
request: GetBlockAccessLists,
|
||||
/// The channel to send the response for block access lists.
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
},
|
||||
}
|
||||
|
||||
// === impl PeerRequest ===
|
||||
@@ -272,9 +282,19 @@ impl<N: NetworkPrimitives> PeerRequest<N> {
|
||||
Self::GetReceipts { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetReceipts69 { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetReceipts70 { response, .. } => response.send(Err(err)).ok(),
|
||||
Self::GetBlockAccessLists { response, .. } => response.send(Err(err)).ok(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if this request is supported for the negotiated eth protocol version.
|
||||
#[inline]
|
||||
pub fn is_supported_by_eth_version(&self, version: EthVersion) -> bool {
|
||||
match self {
|
||||
Self::GetBlockAccessLists { .. } => version >= EthVersion::Eth71,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`EthMessage`] for this type
|
||||
pub fn create_request_message(&self, request_id: u64) -> EthMessage<N> {
|
||||
match self {
|
||||
@@ -299,6 +319,12 @@ impl<N: NetworkPrimitives> PeerRequest<N> {
|
||||
Self::GetReceipts70 { request, .. } => {
|
||||
EthMessage::GetReceipts70(RequestPair { request_id, message: request.clone() })
|
||||
}
|
||||
Self::GetBlockAccessLists { request, .. } => {
|
||||
EthMessage::GetBlockAccessLists(RequestPair {
|
||||
request_id,
|
||||
message: request.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,3 +375,18 @@ impl<R> fmt::Debug for PeerRequestSender<R> {
|
||||
f.debug_struct("PeerRequestSender").field("peer_id", &self.peer_id).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_block_access_lists_version_support() {
|
||||
let (tx, _rx) = oneshot::channel();
|
||||
let req: PeerRequest<EthNetworkPrimitives> =
|
||||
PeerRequest::GetBlockAccessLists { request: GetBlockAccessLists(vec![]), response: tx };
|
||||
|
||||
assert!(!req.is_supported_by_eth_version(EthVersion::Eth70));
|
||||
assert!(req.is_supported_by_eth_version(EthVersion::Eth71));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ use crate::{
|
||||
};
|
||||
use alloy_consensus::{BlockHeader, ReceiptWithBloom};
|
||||
use alloy_eips::BlockHashOrNumber;
|
||||
use alloy_primitives::Bytes;
|
||||
use alloy_rlp::Encodable;
|
||||
use futures::StreamExt;
|
||||
use reth_eth_wire::{
|
||||
BlockBodies, BlockHeaders, EthNetworkPrimitives, GetBlockBodies, GetBlockHeaders, GetNodeData,
|
||||
GetReceipts, GetReceipts70, HeadersDirection, NetworkPrimitives, NodeData, Receipts,
|
||||
Receipts69, Receipts70,
|
||||
BlockAccessLists, BlockBodies, BlockHeaders, EthNetworkPrimitives, GetBlockAccessLists,
|
||||
GetBlockBodies, GetBlockHeaders, GetNodeData, GetReceipts, GetReceipts70, HeadersDirection,
|
||||
NetworkPrimitives, NodeData, Receipts, Receipts69, Receipts70,
|
||||
};
|
||||
use reth_network_api::test_utils::PeersHandle;
|
||||
use reth_network_p2p::error::RequestResult;
|
||||
@@ -281,6 +282,19 @@ where
|
||||
let _ = response.send(Ok(Receipts70 { last_block_incomplete, receipts }));
|
||||
}
|
||||
|
||||
/// Handles [`GetBlockAccessLists`] queries.
|
||||
///
|
||||
/// For now this returns one empty BAL per requested hash.
|
||||
fn on_block_access_lists_request(
|
||||
&self,
|
||||
_peer_id: PeerId,
|
||||
request: GetBlockAccessLists,
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
) {
|
||||
let access_lists = request.0.into_iter().map(|_| Bytes::new()).collect();
|
||||
let _ = response.send(Ok(BlockAccessLists(access_lists)));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_receipts_response<T, F>(&self, request: GetReceipts, transform_fn: F) -> Vec<Vec<T>>
|
||||
where
|
||||
@@ -352,6 +366,9 @@ where
|
||||
IncomingEthRequest::GetReceipts70 { peer_id, request, response } => {
|
||||
this.on_receipts70_request(peer_id, request, response)
|
||||
}
|
||||
IncomingEthRequest::GetBlockAccessLists { peer_id, request, response } => {
|
||||
this.on_block_access_lists_request(peer_id, request, response)
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -437,4 +454,15 @@ pub enum IncomingEthRequest<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// The channel sender for the response containing Receipts70.
|
||||
response: oneshot::Sender<RequestResult<Receipts70<N::Receipt>>>,
|
||||
},
|
||||
/// Request Block Access Lists from the peer.
|
||||
///
|
||||
/// The response should be sent through the channel.
|
||||
GetBlockAccessLists {
|
||||
/// The ID of the peer to request block access lists from.
|
||||
peer_id: PeerId,
|
||||
/// The requested block hashes.
|
||||
request: GetBlockAccessLists,
|
||||
/// The channel sender for the response containing block access lists.
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -551,6 +551,13 @@ impl<N: NetworkPrimitives> NetworkManager<N> {
|
||||
response,
|
||||
})
|
||||
}
|
||||
PeerRequest::GetBlockAccessLists { request, response } => {
|
||||
self.delegate_eth_request(IncomingEthRequest::GetBlockAccessLists {
|
||||
peer_id,
|
||||
request,
|
||||
response,
|
||||
})
|
||||
}
|
||||
PeerRequest::GetPooledTransactions { request, response } => {
|
||||
self.notify_tx_manager(NetworkTransactionEvent::GetPooledTransactions {
|
||||
peer_id,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! An `RLPx` stream is multiplexed via the prepended message-id of a framed message.
|
||||
//! Capabilities are exchanged via the `RLPx` `Hello` message as pairs of `(id, version)`, <https://github.com/ethereum/devp2p/blob/master/rlpx.md#capability-messaging>
|
||||
|
||||
use crate::types::{Receipts69, Receipts70};
|
||||
use crate::types::{BlockAccessLists, Receipts69, Receipts70};
|
||||
use alloy_consensus::{BlockHeader, ReceiptWithBloom};
|
||||
use alloy_primitives::{Bytes, B256};
|
||||
use futures::FutureExt;
|
||||
@@ -121,6 +121,11 @@ pub enum PeerResponse<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
/// The receiver channel for the response to a receipts request.
|
||||
response: oneshot::Receiver<RequestResult<Receipts70<N::Receipt>>>,
|
||||
},
|
||||
/// Represents a response to a request for block access lists.
|
||||
BlockAccessLists {
|
||||
/// The receiver channel for the response to a block access lists request.
|
||||
response: oneshot::Receiver<RequestResult<BlockAccessLists>>,
|
||||
},
|
||||
}
|
||||
|
||||
// === impl PeerResponse ===
|
||||
@@ -160,6 +165,10 @@ impl<N: NetworkPrimitives> PeerResponse<N> {
|
||||
Ok(res) => PeerResponseResult::Receipts70(res),
|
||||
Err(err) => PeerResponseResult::Receipts70(Err(err.into())),
|
||||
},
|
||||
Self::BlockAccessLists { response } => match ready!(response.poll_unpin(cx)) {
|
||||
Ok(res) => PeerResponseResult::BlockAccessLists(res),
|
||||
Err(err) => PeerResponseResult::BlockAccessLists(Err(err.into())),
|
||||
},
|
||||
};
|
||||
Poll::Ready(res)
|
||||
}
|
||||
@@ -182,6 +191,8 @@ pub enum PeerResponseResult<N: NetworkPrimitives = EthNetworkPrimitives> {
|
||||
Receipts69(RequestResult<Vec<Vec<N::Receipt>>>),
|
||||
/// Represents a result containing receipts or an error for eth/70.
|
||||
Receipts70(RequestResult<Receipts70<N::Receipt>>),
|
||||
/// Represents a result containing block access lists or an error.
|
||||
BlockAccessLists(RequestResult<BlockAccessLists>),
|
||||
}
|
||||
|
||||
// === impl PeerResponseResult ===
|
||||
@@ -226,6 +237,13 @@ impl<N: NetworkPrimitives> PeerResponseResult<N> {
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
Self::BlockAccessLists(resp) => match resp {
|
||||
Ok(res) => {
|
||||
let request = RequestPair { request_id: id, message: res };
|
||||
Ok(EthMessage::BlockAccessLists(request))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +257,7 @@ impl<N: NetworkPrimitives> PeerResponseResult<N> {
|
||||
Self::Receipts(res) => res.as_ref().err(),
|
||||
Self::Receipts69(res) => res.as_ref().err(),
|
||||
Self::Receipts70(res) => res.as_ref().err(),
|
||||
Self::BlockAccessLists(res) => res.as_ref().err(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -282,6 +282,12 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
EthMessage::Receipts70(resp) => {
|
||||
on_response!(resp, GetReceipts70)
|
||||
}
|
||||
EthMessage::GetBlockAccessLists(req) => {
|
||||
on_request!(req, BlockAccessLists, GetBlockAccessLists)
|
||||
}
|
||||
EthMessage::BlockAccessLists(resp) => {
|
||||
on_response!(resp, GetBlockAccessLists)
|
||||
}
|
||||
EthMessage::BlockRangeUpdate(msg) => {
|
||||
// Validate that earliest <= latest according to the spec
|
||||
if msg.earliest > msg.latest {
|
||||
@@ -316,9 +322,22 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
|
||||
/// 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) {
|
||||
debug!(
|
||||
target: "net",
|
||||
?request,
|
||||
peer_id=?self.remote_peer_id,
|
||||
?version,
|
||||
"Request not supported for negotiated eth version",
|
||||
);
|
||||
request.send_err_response(RequestError::UnsupportedCapability);
|
||||
return;
|
||||
}
|
||||
|
||||
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(self.conn.version());
|
||||
let msg = request.create_request_message(request_id).map_versioned(version);
|
||||
|
||||
self.queued_outgoing.push_back(msg.into());
|
||||
let req = InflightRequest {
|
||||
@@ -329,6 +348,11 @@ impl<N: NetworkPrimitives> ActiveSession<N> {
|
||||
self.inflight_requests.insert(request_id, req);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_request_supported_for_version(request: &PeerRequest<N>, version: EthVersion) -> bool {
|
||||
request.is_supported_by_eth_version(version)
|
||||
}
|
||||
|
||||
/// Handle a message received from the internal network
|
||||
fn on_internal_peer_message(&mut self, msg: PeerMessage<N>) {
|
||||
match msg {
|
||||
@@ -938,9 +962,9 @@ mod tests {
|
||||
use reth_chainspec::MAINNET;
|
||||
use reth_ecies::stream::ECIESStream;
|
||||
use reth_eth_wire::{
|
||||
handshake::EthHandshake, EthNetworkPrimitives, EthStream, GetBlockBodies,
|
||||
HelloMessageWithProtocols, P2PStream, StatusBuilder, UnauthedEthStream, UnauthedP2PStream,
|
||||
UnifiedStatus,
|
||||
handshake::EthHandshake, EthNetworkPrimitives, EthStream, GetBlockAccessLists,
|
||||
GetBlockBodies, HelloMessageWithProtocols, P2PStream, StatusBuilder, UnauthedEthStream,
|
||||
UnauthedP2PStream, UnifiedStatus,
|
||||
};
|
||||
use reth_ethereum_forks::EthereumHardfork;
|
||||
use reth_network_peers::pk2id;
|
||||
@@ -1240,6 +1264,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reject_bal_request_for_eth70() {
|
||||
let (tx, _rx) = oneshot::channel();
|
||||
let request: PeerRequest<EthNetworkPrimitives> =
|
||||
PeerRequest::GetBlockAccessLists { request: GetBlockAccessLists(vec![]), response: tx };
|
||||
|
||||
assert!(!ActiveSession::<EthNetworkPrimitives>::is_request_supported_for_version(
|
||||
&request,
|
||||
EthVersion::Eth70
|
||||
));
|
||||
assert!(ActiveSession::<EthNetworkPrimitives>::is_request_supported_for_version(
|
||||
&request,
|
||||
EthVersion::Eth71
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_keep_alive() {
|
||||
let mut builder = SessionBuilder::default();
|
||||
|
||||
@@ -908,7 +908,7 @@ pub(crate) async fn start_pending_incoming_session<N: NetworkPrimitives>(
|
||||
}
|
||||
|
||||
/// Starts the authentication process for a connection initiated by a remote peer.
|
||||
#[instrument(level = "trace", target = "net::network", skip_all, fields(%remote_addr, peer_id))]
|
||||
#[instrument(level = "trace", target = "net::network", skip_all, fields(%remote_addr, peer_id = ?remote_peer_id))]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
async fn start_pending_outbound_session<N: NetworkPrimitives>(
|
||||
handshake: Arc<dyn EthRlpxHandshake>,
|
||||
|
||||
@@ -1949,7 +1949,7 @@ impl PooledTransactionsHashesBuilder {
|
||||
fn new(version: EthVersion) -> Self {
|
||||
match version {
|
||||
EthVersion::Eth66 | EthVersion::Eth67 => Self::Eth66(Default::default()),
|
||||
EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70 => {
|
||||
EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70 | EthVersion::Eth71 => {
|
||||
Self::Eth68(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,13 @@ use reth_network::{
|
||||
};
|
||||
use reth_network_api::{
|
||||
events::{PeerEvent, SessionInfo},
|
||||
NetworkInfo, Peers, PeersInfo,
|
||||
NetworkInfo, PeerKind, Peers, PeersInfo,
|
||||
};
|
||||
use reth_network_p2p::{
|
||||
headers::client::{HeadersClient, HeadersRequest},
|
||||
sync::{NetworkSyncUpdater, SyncState},
|
||||
};
|
||||
use reth_network_peers::{mainnet_nodes, NodeRecord, TrustedPeer};
|
||||
use reth_network_types::peers::config::PeerBackoffDurations;
|
||||
use reth_provider::test_utils::MockEthProvider;
|
||||
use reth_storage_api::noop::NoopProvider;
|
||||
use reth_tracing::init_test_tracing;
|
||||
@@ -380,10 +379,7 @@ async fn test_trusted_peer_only() {
|
||||
let _handle = net.spawn();
|
||||
|
||||
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
|
||||
let peers_config = PeersConfig::default()
|
||||
.with_backoff_durations(PeerBackoffDurations::test())
|
||||
.with_ban_duration(Duration::from_millis(200))
|
||||
.with_trusted_nodes_only(true);
|
||||
let peers_config = PeersConfig::test().with_trusted_nodes_only(true);
|
||||
|
||||
let config = NetworkConfigBuilder::eth(secret_key)
|
||||
.listener_port(0)
|
||||
@@ -405,8 +401,8 @@ async fn test_trusted_peer_only() {
|
||||
// connect to an untrusted peer should fail.
|
||||
handle.add_peer(*handle0.peer_id(), handle0.local_addr());
|
||||
|
||||
// wait 1 second, the number of connection is still 0.
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
// wait 500ms, the number of connection is still 0.
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
assert_eq!(handle.num_connected_peers(), 0);
|
||||
|
||||
// add to trusted peer.
|
||||
@@ -419,17 +415,22 @@ async fn test_trusted_peer_only() {
|
||||
// only receive connections from trusted peers.
|
||||
handle1.add_peer(*handle.peer_id(), handle.local_addr());
|
||||
|
||||
// wait 1 second, the number of connections is still 1, because peer1 is untrusted.
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
// wait 500ms, the number of connections is still 1, because peer1 is untrusted.
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
assert_eq!(handle.num_connected_peers(), 1);
|
||||
|
||||
// remove handle from handle1's peer list to prevent a competing outgoing connection attempt
|
||||
// from handle1 racing with handle's outgoing connection below, which can cause duplicate
|
||||
// session resolution to drop a connection
|
||||
handle1.remove_peer(*handle.peer_id(), PeerKind::Basic);
|
||||
|
||||
handle.add_trusted_peer(*handle1.peer_id(), handle1.local_addr());
|
||||
|
||||
// wait for the next session established event to check the handle1 incoming connection
|
||||
let outgoing_peer_id1 = event_stream.next_session_established().await.unwrap();
|
||||
assert_eq!(outgoing_peer_id1, *handle1.peer_id());
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
assert_eq!(handle.num_connected_peers(), 2);
|
||||
|
||||
// check that handle0 and handle1 both have peers.
|
||||
@@ -441,8 +442,7 @@ async fn test_trusted_peer_only() {
|
||||
async fn test_network_state_change() {
|
||||
let net = Testnet::create(1).await;
|
||||
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
|
||||
let peers_config =
|
||||
PeersConfig::default().with_refill_slots_interval(Duration::from_millis(500));
|
||||
let peers_config = PeersConfig::test();
|
||||
|
||||
let config = NetworkConfigBuilder::eth(secret_key)
|
||||
.listener_port(0)
|
||||
@@ -466,16 +466,16 @@ async fn test_network_state_change() {
|
||||
|
||||
handle.add_peer(*handle0.peer_id(), handle0.local_addr());
|
||||
|
||||
// wait 2 seconds, the number of connections is still 0, because network is Hibernate.
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
// wait 500ms, the number of connections is still 0, because network is Hibernate.
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
assert_eq!(handle.num_connected_peers(), 0);
|
||||
|
||||
// Set network state to Active.
|
||||
handle.set_network_active();
|
||||
|
||||
// wait 2 seconds, the number of connections should be 1, because network is Active and outbound
|
||||
// wait 500ms, the number of connections should be 1, because network is Active and outbound
|
||||
// slot should be filled.
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
assert_eq!(handle.num_connected_peers(), 1);
|
||||
}
|
||||
|
||||
@@ -483,7 +483,7 @@ async fn test_network_state_change() {
|
||||
async fn test_exceed_outgoing_connections() {
|
||||
let net = Testnet::create(2).await;
|
||||
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
|
||||
let peers_config = PeersConfig::default().with_max_outbound(1);
|
||||
let peers_config = PeersConfig::test().with_max_outbound(1);
|
||||
|
||||
let config = NetworkConfigBuilder::eth(secret_key)
|
||||
.listener_port(0)
|
||||
@@ -514,9 +514,9 @@ async fn test_exceed_outgoing_connections() {
|
||||
|
||||
handle.add_peer(*handle1.peer_id(), handle1.local_addr());
|
||||
|
||||
// wait 2 seconds, the number of connections is still 1, indicating that the max outbound is in
|
||||
// wait 500ms, the number of connections is still 1, indicating that the max outbound is in
|
||||
// effect.
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
assert_eq!(handle.num_connected_peers(), 1);
|
||||
}
|
||||
|
||||
@@ -524,7 +524,7 @@ async fn test_exceed_outgoing_connections() {
|
||||
async fn test_disconnect_incoming_when_exceeded_incoming_connections() {
|
||||
let net = Testnet::create(1).await;
|
||||
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
|
||||
let peers_config = PeersConfig::default().with_max_inbound(0);
|
||||
let peers_config = PeersConfig::test().with_max_inbound(0);
|
||||
|
||||
let config = NetworkConfigBuilder::eth(secret_key)
|
||||
.listener_port(0)
|
||||
@@ -543,7 +543,7 @@ async fn test_disconnect_incoming_when_exceeded_incoming_connections() {
|
||||
tokio::task::spawn(network);
|
||||
let net_handle = net.spawn();
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
assert_eq!(handle.num_connected_peers(), 0);
|
||||
|
||||
@@ -623,15 +623,15 @@ async fn test_rejected_by_already_connect() {
|
||||
// incoming connection from the same peer should be rejected by already connected
|
||||
// and num_inbount should still be 1
|
||||
other_peer_handle1.add_peer(*handle.peer_id(), handle.local_addr());
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// incoming connection from other_peer2 should succeed
|
||||
other_peer_handle2.add_peer(*handle.peer_id(), handle.local_addr());
|
||||
let peer_id = events.next_session_established().await.unwrap();
|
||||
assert_eq!(peer_id, *other_peer_handle2.peer_id());
|
||||
|
||||
// wait 2 seconds and check that other_peer2 is not rejected by TooManyPeers
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
// wait 500ms and check that other_peer2 is not rejected by TooManyPeers
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
assert_eq!(handle.num_connected_peers(), 2);
|
||||
}
|
||||
|
||||
@@ -641,7 +641,7 @@ async fn new_random_peer(
|
||||
) -> NetworkManager<EthNetworkPrimitives> {
|
||||
let secret_key = SecretKey::new(&mut rand_08::thread_rng());
|
||||
let peers_config =
|
||||
PeersConfig::default().with_max_inbound(max_in_bound).with_trusted_nodes(trusted_nodes);
|
||||
PeersConfig::test().with_max_inbound(max_in_bound).with_trusted_nodes(trusted_nodes);
|
||||
|
||||
let config = NetworkConfigBuilder::new(secret_key)
|
||||
.listener_port(0)
|
||||
@@ -775,7 +775,7 @@ async fn test_reconnect_trusted() {
|
||||
|
||||
// Await that handle1 (trusted peer) reconnects automatically
|
||||
let reconnect_result =
|
||||
tokio::time::timeout(Duration::from_secs(60), listener0.next_session_established()).await;
|
||||
tokio::time::timeout(Duration::from_secs(10), listener0.next_session_established()).await;
|
||||
|
||||
match reconnect_result {
|
||||
Ok(Some(peer)) => {
|
||||
|
||||
@@ -24,7 +24,6 @@ reth-db-common.workspace = true
|
||||
reth-downloaders.workspace = true
|
||||
reth-engine-local.workspace = true
|
||||
reth-engine-primitives.workspace = true
|
||||
reth-engine-service.workspace = true
|
||||
reth-engine-tree.workspace = true
|
||||
reth-engine-util.workspace = true
|
||||
reth-evm.workspace = true
|
||||
|
||||
@@ -11,10 +11,10 @@ use crate::{
|
||||
use alloy_consensus::BlockHeader;
|
||||
use futures::{stream_select, FutureExt, StreamExt};
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardforks};
|
||||
use reth_engine_service::service::{ChainEvent, EngineService};
|
||||
use reth_engine_tree::{
|
||||
chain::FromOrchestrator,
|
||||
engine::{EngineApiRequest, EngineRequestHandler},
|
||||
chain::{ChainEvent, FromOrchestrator},
|
||||
engine::{EngineApiKind, EngineApiRequest, EngineRequestHandler},
|
||||
launch::build_engine_orchestrator,
|
||||
tree::TreeConfig,
|
||||
};
|
||||
use reth_engine_util::EngineMessageStreamExt;
|
||||
@@ -219,9 +219,15 @@ impl EngineNodeLauncher {
|
||||
// during this run.
|
||||
.maybe_store_messages(node_config.debug.engine_api_store.clone());
|
||||
|
||||
let mut engine_service = EngineService::new(
|
||||
let engine_kind = if ctx.chain_spec().is_optimism() {
|
||||
EngineApiKind::OpStack
|
||||
} else {
|
||||
EngineApiKind::Ethereum
|
||||
};
|
||||
|
||||
let mut orchestrator = build_engine_orchestrator(
|
||||
engine_kind,
|
||||
consensus.clone(),
|
||||
ctx.chain_spec(),
|
||||
network_client.clone(),
|
||||
Box::pin(consensus_engine_stream),
|
||||
pipeline,
|
||||
@@ -290,7 +296,7 @@ impl EngineNodeLauncher {
|
||||
if let Some(initial_target) = initial_target {
|
||||
debug!(target: "reth::cli", %initial_target, "start backfill sync");
|
||||
// network_handle's sync state is already initialized at Syncing
|
||||
engine_service.orchestrator_mut().start_backfill_sync(initial_target);
|
||||
orchestrator.start_backfill_sync(initial_target);
|
||||
} else if startup_sync_state_idle {
|
||||
network_handle.update_sync_state(SyncState::Idle);
|
||||
}
|
||||
@@ -303,7 +309,7 @@ impl EngineNodeLauncher {
|
||||
// the CL
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = engine_service.next() => {
|
||||
event = orchestrator.next() => {
|
||||
let Some(event) = event else { break };
|
||||
debug!(target: "reth::cli", "Event: {event}");
|
||||
match event {
|
||||
@@ -353,13 +359,13 @@ impl EngineNodeLauncher {
|
||||
payload = built_payloads.select_next_some() => {
|
||||
if let Some(executed_block) = payload.executed_block() {
|
||||
debug!(target: "reth::cli", block=?executed_block.recovered_block.num_hash(), "inserting built payload");
|
||||
engine_service.orchestrator_mut().handler_mut().handler_mut().on_event(EngineApiRequest::InsertExecutedBlock(executed_block.into_executed_payload()).into());
|
||||
orchestrator.handler_mut().handler_mut().on_event(EngineApiRequest::InsertExecutedBlock(executed_block.into_executed_payload()).into());
|
||||
}
|
||||
}
|
||||
shutdown_req = &mut shutdown_rx => {
|
||||
if let Ok(req) = shutdown_req {
|
||||
debug!(target: "reth::cli", "received engine shutdown request");
|
||||
engine_service.orchestrator_mut().handler_mut().handler_mut().on_event(
|
||||
orchestrator.handler_mut().handler_mut().on_event(
|
||||
FromOrchestrator::Terminate { tx: req.done_tx }.into()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ pub struct DefaultEngineValues {
|
||||
allow_unwind_canonical_header: bool,
|
||||
storage_worker_count: Option<usize>,
|
||||
account_worker_count: Option<usize>,
|
||||
prewarming_threads: Option<usize>,
|
||||
disable_proof_v2: bool,
|
||||
cache_metrics_disabled: bool,
|
||||
disable_trie_cache: bool,
|
||||
@@ -169,6 +170,12 @@ impl DefaultEngineValues {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the default prewarming thread count
|
||||
pub const fn with_prewarming_threads(mut self, v: Option<usize>) -> Self {
|
||||
self.prewarming_threads = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to disable proof V2 by default
|
||||
pub const fn with_disable_proof_v2(mut self, v: bool) -> Self {
|
||||
self.disable_proof_v2 = v;
|
||||
@@ -233,6 +240,7 @@ impl Default for DefaultEngineValues {
|
||||
allow_unwind_canonical_header: false,
|
||||
storage_worker_count: None,
|
||||
account_worker_count: None,
|
||||
prewarming_threads: None,
|
||||
disable_proof_v2: false,
|
||||
cache_metrics_disabled: false,
|
||||
disable_trie_cache: false,
|
||||
@@ -360,6 +368,11 @@ pub struct EngineArgs {
|
||||
#[arg(long = "engine.account-worker-count", default_value = Resettable::from(DefaultEngineValues::get_global().account_worker_count.map(|v| v.to_string().into())))]
|
||||
pub account_worker_count: Option<usize>,
|
||||
|
||||
/// Configure the number of prewarming threads.
|
||||
/// If not specified, defaults to available parallelism.
|
||||
#[arg(long = "engine.prewarming-threads", default_value = Resettable::from(DefaultEngineValues::get_global().prewarming_threads.map(|v| v.to_string().into())))]
|
||||
pub prewarming_threads: Option<usize>,
|
||||
|
||||
/// Disable V2 storage proofs for state root calculations
|
||||
#[arg(long = "engine.disable-proof-v2", default_value_t = DefaultEngineValues::get_global().disable_proof_v2)]
|
||||
pub disable_proof_v2: bool,
|
||||
@@ -424,6 +437,7 @@ impl Default for EngineArgs {
|
||||
allow_unwind_canonical_header,
|
||||
storage_worker_count,
|
||||
account_worker_count,
|
||||
prewarming_threads,
|
||||
disable_proof_v2,
|
||||
cache_metrics_disabled,
|
||||
disable_trie_cache,
|
||||
@@ -455,6 +469,7 @@ impl Default for EngineArgs {
|
||||
allow_unwind_canonical_header,
|
||||
storage_worker_count,
|
||||
account_worker_count,
|
||||
prewarming_threads,
|
||||
disable_proof_v2,
|
||||
cache_metrics_disabled,
|
||||
disable_trie_cache,
|
||||
@@ -546,6 +561,7 @@ mod tests {
|
||||
allow_unwind_canonical_header: true,
|
||||
storage_worker_count: Some(16),
|
||||
account_worker_count: Some(8),
|
||||
prewarming_threads: Some(4),
|
||||
disable_proof_v2: false,
|
||||
cache_metrics_disabled: true,
|
||||
disable_trie_cache: true,
|
||||
@@ -582,6 +598,8 @@ mod tests {
|
||||
"16",
|
||||
"--engine.account-worker-count",
|
||||
"8",
|
||||
"--engine.prewarming-threads",
|
||||
"4",
|
||||
"--engine.disable-cache-metrics",
|
||||
"--engine.disable-trie-cache",
|
||||
"--engine.sparse-trie-prune-depth",
|
||||
|
||||
@@ -354,7 +354,7 @@ pub(crate) fn parse_receipts_log_filter(
|
||||
) -> Result<ReceiptsLogPruneConfig, ReceiptsLogError> {
|
||||
let mut config = BTreeMap::new();
|
||||
// Split out each of the filters.
|
||||
let filters = value.split(',');
|
||||
let filters = value.split(',').map(str::trim);
|
||||
for filter in filters {
|
||||
let parts: Vec<&str> = filter.split(':').collect();
|
||||
if parts.len() < 2 {
|
||||
@@ -450,6 +450,23 @@ mod tests {
|
||||
assert_eq!(config.0.get(&addr3), Some(&PruneMode::Before(5000000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_receipts_log_filter_with_spaces() {
|
||||
// Verify that spaces after commas are handled correctly
|
||||
let filters = "0x0000000000000000000000000000000000000001:full, 0x0000000000000000000000000000000000000002:distance:1000";
|
||||
|
||||
let result = parse_receipts_log_filter(filters);
|
||||
assert!(result.is_ok());
|
||||
let config = result.unwrap();
|
||||
assert_eq!(config.0.len(), 2);
|
||||
|
||||
let addr1: Address = "0x0000000000000000000000000000000000000001".parse().unwrap();
|
||||
let addr2: Address = "0x0000000000000000000000000000000000000002".parse().unwrap();
|
||||
|
||||
assert_eq!(config.0.get(&addr1), Some(&PruneMode::Full));
|
||||
assert_eq!(config.0.get(&addr2), Some(&PruneMode::Distance(1000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_receipts_log_filter_invalid_filter_format() {
|
||||
let result = parse_receipts_log_filter("invalid_format");
|
||||
|
||||
@@ -20,6 +20,7 @@ use reth_primitives_traits::TxTy;
|
||||
use reth_rpc_convert::RpcTxReq;
|
||||
use reth_rpc_eth_types::FillTransaction;
|
||||
use reth_rpc_server_types::{result::internal_rpc_err, ToRpcResult};
|
||||
use std::collections::HashMap;
|
||||
use tracing::trace;
|
||||
|
||||
/// Helper trait, unifies functionality that must be supported to implement all RPC methods for
|
||||
@@ -201,6 +202,14 @@ pub trait EthApi<
|
||||
block_number: Option<BlockId>,
|
||||
) -> RpcResult<B256>;
|
||||
|
||||
/// Returns values from multiple storage positions across multiple addresses.
|
||||
#[method(name = "getStorageValues")]
|
||||
async fn storage_values(
|
||||
&self,
|
||||
requests: HashMap<Address, Vec<JsonStorageKey>>,
|
||||
block_number: Option<BlockId>,
|
||||
) -> RpcResult<HashMap<Address, Vec<B256>>>;
|
||||
|
||||
/// Returns the number of transactions sent from an address at given block number.
|
||||
#[method(name = "getTransactionCount")]
|
||||
async fn transaction_count(
|
||||
@@ -651,6 +660,16 @@ where
|
||||
Ok(EthState::storage_at(self, address, index, block_number).await?)
|
||||
}
|
||||
|
||||
/// Handler for: `eth_getStorageValues`
|
||||
async fn storage_values(
|
||||
&self,
|
||||
requests: HashMap<Address, Vec<JsonStorageKey>>,
|
||||
block_number: Option<BlockId>,
|
||||
) -> RpcResult<HashMap<Address, Vec<B256>>> {
|
||||
trace!(target: "rpc::eth", ?block_number, "Serving eth_getStorageValues");
|
||||
Ok(EthState::storage_values(self, requests, block_number).await?)
|
||||
}
|
||||
|
||||
/// Handler for: `eth_getTransactionCount`
|
||||
async fn transaction_count(
|
||||
&self,
|
||||
|
||||
@@ -16,11 +16,13 @@ use reth_rpc_convert::RpcConvert;
|
||||
use reth_rpc_eth_types::{
|
||||
error::FromEvmError, EthApiError, PendingBlockEnv, RpcInvalidTransactionError,
|
||||
};
|
||||
use reth_rpc_server_types::constants::DEFAULT_MAX_STORAGE_VALUES_SLOTS;
|
||||
use reth_storage_api::{
|
||||
BlockIdReader, BlockNumReader, BlockReaderIdExt, StateProvider, StateProviderBox,
|
||||
StateProviderFactory,
|
||||
};
|
||||
use reth_transaction_pool::TransactionPool;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Helper methods for `eth_` methods relating to state (accounts).
|
||||
pub trait EthState: LoadState + SpawnBlocking {
|
||||
@@ -83,6 +85,47 @@ pub trait EthState: LoadState + SpawnBlocking {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns values from multiple storage positions across multiple addresses.
|
||||
///
|
||||
/// Enforces a cap on total slot count (sum of all slot arrays) and returns an error if
|
||||
/// exceeded.
|
||||
fn storage_values(
|
||||
&self,
|
||||
requests: HashMap<Address, Vec<JsonStorageKey>>,
|
||||
block_id: Option<BlockId>,
|
||||
) -> impl Future<Output = Result<HashMap<Address, Vec<B256>>, Self::Error>> + Send {
|
||||
async move {
|
||||
let total_slots: usize = requests.values().map(|slots| slots.len()).sum();
|
||||
if total_slots > DEFAULT_MAX_STORAGE_VALUES_SLOTS {
|
||||
return Err(Self::Error::from_eth_err(EthApiError::InvalidParams(
|
||||
format!(
|
||||
"total slot count {total_slots} exceeds limit {DEFAULT_MAX_STORAGE_VALUES_SLOTS}",
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
self.spawn_blocking_io_fut(move |this| async move {
|
||||
let state = this.state_at_block_id_or_latest(block_id).await?;
|
||||
|
||||
let mut result = HashMap::with_capacity(requests.len());
|
||||
for (address, slots) in requests {
|
||||
let mut values = Vec::with_capacity(slots.len());
|
||||
for slot in &slots {
|
||||
let value = state
|
||||
.storage(address, slot.as_b256())
|
||||
.map_err(Self::Error::from_eth_err)?
|
||||
.unwrap_or_default();
|
||||
values.push(B256::new(value.to_be_bytes()));
|
||||
}
|
||||
result.insert(address, values);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns values stored of given account, with Merkle-proof, at given blocknumber.
|
||||
fn get_proof(
|
||||
&self,
|
||||
|
||||
@@ -55,6 +55,9 @@ pub const DEFAULT_ENGINE_API_IPC_ENDPOINT: &str = "/tmp/reth_engine_api.ipc";
|
||||
/// The default limit for blocks count in `eth_simulateV1`.
|
||||
pub const DEFAULT_MAX_SIMULATE_BLOCKS: u64 = 256;
|
||||
|
||||
/// The default maximum number of total storage slots for `eth_getStorageValues`.
|
||||
pub const DEFAULT_MAX_STORAGE_VALUES_SLOTS: usize = 1024;
|
||||
|
||||
/// The default eth historical proof window.
|
||||
pub const DEFAULT_ETH_PROOF_WINDOW: u64 = 0;
|
||||
|
||||
|
||||
@@ -183,10 +183,9 @@ where
|
||||
// channels used to return result of account hashing
|
||||
for chunk in &accounts_cursor.walk(None)?.chunks(WORKER_CHUNK_SIZE) {
|
||||
// An _unordered_ channel to receive results from a rayon job
|
||||
let (tx, rx) = mpsc::channel();
|
||||
channels.push(rx);
|
||||
|
||||
let chunk = chunk.collect::<Result<Vec<_>, _>>()?;
|
||||
let (tx, rx) = mpsc::sync_channel(chunk.len());
|
||||
channels.push(rx);
|
||||
// Spawn the hashing task onto the global rayon pool
|
||||
rayon::spawn(move || {
|
||||
for (address, account) in chunk {
|
||||
|
||||
@@ -110,10 +110,9 @@ where
|
||||
|
||||
for chunk in &storage_cursor.walk(None)?.chunks(WORKER_CHUNK_SIZE) {
|
||||
// An _unordered_ channel to receive results from a rayon job
|
||||
let (tx, rx) = mpsc::channel();
|
||||
channels.push(rx);
|
||||
|
||||
let chunk = chunk.collect::<Result<Vec<_>, _>>()?;
|
||||
let (tx, rx) = mpsc::sync_channel(chunk.len());
|
||||
channels.push(rx);
|
||||
// Spawn the hashing task onto the global rayon pool
|
||||
rayon::spawn(move || {
|
||||
// Cache hashed address since PlainStorageState is sorted by address
|
||||
|
||||
@@ -34,7 +34,7 @@ const BATCH_SIZE: usize = 100_000;
|
||||
const WORKER_CHUNK_SIZE: usize = 100;
|
||||
|
||||
/// Type alias for a sender that transmits the result of sender recovery.
|
||||
type RecoveryResultSender = mpsc::Sender<Result<(u64, Address), Box<SenderRecoveryStageError>>>;
|
||||
type RecoveryResultSender = mpsc::SyncSender<Result<(u64, Address), Box<SenderRecoveryStageError>>>;
|
||||
|
||||
/// The sender recovery stage iterates over existing transactions,
|
||||
/// recovers the transaction signer and stores them
|
||||
@@ -245,7 +245,7 @@ where
|
||||
.step_by(WORKER_CHUNK_SIZE)
|
||||
.map(|start| {
|
||||
let range = start..std::cmp::min(start + WORKER_CHUNK_SIZE as u64, tx_range.end);
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let (tx, rx) = mpsc::sync_channel((range.end - range.start) as usize);
|
||||
// Range and channel sender will be sent to rayon worker
|
||||
((range, tx), rx)
|
||||
})
|
||||
|
||||
@@ -282,7 +282,7 @@ where
|
||||
level = "debug",
|
||||
target = "providers::state::overlay",
|
||||
skip_all,
|
||||
fields(db_tip_block)
|
||||
fields(%db_tip_block)
|
||||
)]
|
||||
fn calculate_overlay(
|
||||
&self,
|
||||
|
||||
@@ -39,7 +39,7 @@ impl<T: BlockExecutionWriter> BlockExecutionWriter for &T {
|
||||
}
|
||||
|
||||
/// Block Writer
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait BlockWriter {
|
||||
/// The body this writer can write.
|
||||
type Block: Block;
|
||||
|
||||
@@ -9,7 +9,7 @@ use reth_primitives_traits::{Account, StorageEntry};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
/// Hashing Writer
|
||||
#[auto_impl(&, Box)]
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait HashingWriter: Send {
|
||||
/// Unwind and clear account hashing.
|
||||
///
|
||||
|
||||
@@ -7,7 +7,7 @@ use reth_db_models::AccountBeforeTx;
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
/// History Writer
|
||||
#[auto_impl(&, Box)]
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait HistoryWriter: Send {
|
||||
/// Unwind and clear account history indices.
|
||||
///
|
||||
|
||||
@@ -11,7 +11,7 @@ pub mod keys {
|
||||
}
|
||||
|
||||
/// Client trait for reading node metadata from the database.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait MetadataProvider: Send {
|
||||
/// Get a metadata value by key
|
||||
fn get_metadata(&self, key: &str) -> ProviderResult<Option<Vec<u8>>>;
|
||||
|
||||
@@ -3,7 +3,7 @@ use reth_prune_types::{PruneCheckpoint, PruneSegment};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
/// The trait for fetching prune checkpoint related data.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait PruneCheckpointReader: Send {
|
||||
/// Fetch the prune checkpoint for the given segment.
|
||||
fn get_prune_checkpoint(
|
||||
@@ -16,7 +16,7 @@ pub trait PruneCheckpointReader: Send {
|
||||
}
|
||||
|
||||
/// The trait for updating prune checkpoint related data.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait PruneCheckpointWriter {
|
||||
/// Save prune checkpoint.
|
||||
fn save_prune_checkpoint(
|
||||
|
||||
@@ -4,7 +4,7 @@ use reth_stages_types::{StageCheckpoint, StageId};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
/// The trait for fetching stage checkpoint related data.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait StageCheckpointReader: Send {
|
||||
/// Fetch the checkpoint for the given stage.
|
||||
fn get_stage_checkpoint(&self, id: StageId) -> ProviderResult<Option<StageCheckpoint>>;
|
||||
@@ -18,7 +18,7 @@ pub trait StageCheckpointReader: Send {
|
||||
}
|
||||
|
||||
/// The trait for updating stage checkpoint related data.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait StageCheckpointWriter {
|
||||
/// Save stage checkpoint.
|
||||
fn save_stage_checkpoint(&self, id: StageId, checkpoint: StageCheckpoint)
|
||||
|
||||
@@ -14,7 +14,7 @@ use reth_trie_common::HashedPostState;
|
||||
use revm_database::BundleState;
|
||||
|
||||
/// This just receives state, or [`ExecutionOutcome`], from the provider
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait StateReader: Send {
|
||||
/// Receipt type in [`ExecutionOutcome`].
|
||||
type Receipt: Send + Sync;
|
||||
@@ -30,7 +30,7 @@ pub trait StateReader: Send {
|
||||
pub type StateProviderBox = Box<dyn StateProvider + Send + 'static>;
|
||||
|
||||
/// An abstraction for a type that provides state data.
|
||||
#[auto_impl(&, Box)]
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait StateProvider:
|
||||
BlockHashReader
|
||||
+ AccountReader
|
||||
@@ -110,14 +110,14 @@ pub trait AccountInfoReader: AccountReader + BytecodeReader {}
|
||||
impl<T: AccountReader + BytecodeReader> AccountInfoReader for T {}
|
||||
|
||||
/// Trait that provides the hashed state from various sources.
|
||||
#[auto_impl(&, Box)]
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait HashedPostStateProvider {
|
||||
/// Returns the `HashedPostState` of the provided [`BundleState`].
|
||||
fn hashed_post_state(&self, bundle_state: &BundleState) -> HashedPostState;
|
||||
}
|
||||
|
||||
/// Trait for reading bytecode associated with a given code hash.
|
||||
#[auto_impl(&, Box)]
|
||||
#[auto_impl(&, Arc, Box)]
|
||||
pub trait BytecodeReader {
|
||||
/// Get account code by its hash
|
||||
fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use reth_db_api::table::Table;
|
||||
|
||||
/// The trait for fetching provider statistics.
|
||||
#[auto_impl::auto_impl(&)]
|
||||
#[auto_impl::auto_impl(&, Arc)]
|
||||
pub trait StatsReader {
|
||||
/// Fetch the number of entries in the corresponding [Table]. Depending on the provider, it may
|
||||
/// route to different data sources other than [Table].
|
||||
|
||||
@@ -3,7 +3,7 @@ use alloc::{
|
||||
vec::Vec,
|
||||
};
|
||||
use alloy_primitives::{Address, BlockNumber, B256, U256};
|
||||
use core::ops::{RangeBounds, RangeInclusive};
|
||||
use core::ops::RangeInclusive;
|
||||
use reth_primitives_traits::{StorageEntry, StorageSlotKey};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
|
||||
@@ -35,7 +35,7 @@ impl From<ChangesetEntry> for StorageEntry {
|
||||
}
|
||||
|
||||
/// Storage reader
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait StorageReader: Send {
|
||||
/// Get plainstate storages for addresses and storage keys.
|
||||
fn plain_state_storages(
|
||||
@@ -61,7 +61,7 @@ pub trait StorageReader: Send {
|
||||
|
||||
/// Storage `ChangeSet` reader
|
||||
#[cfg(feature = "db-api")]
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait StorageChangeSetReader: Send {
|
||||
/// Iterate over storage changesets and return the storage state from before this block.
|
||||
///
|
||||
@@ -91,7 +91,7 @@ pub trait StorageChangeSetReader: Send {
|
||||
/// [`StorageSlotKey::Hashed`] based on the current storage mode.
|
||||
fn storage_changesets_range(
|
||||
&self,
|
||||
range: impl RangeBounds<BlockNumber>,
|
||||
range: impl core::ops::RangeBounds<BlockNumber>,
|
||||
) -> ProviderResult<Vec<(reth_db_api::models::BlockNumberAddress, ChangesetEntry)>>;
|
||||
|
||||
/// Get the total count of all storage changes.
|
||||
|
||||
@@ -40,7 +40,7 @@ pub trait StateRootProvider {
|
||||
}
|
||||
|
||||
/// A type that can compute the storage root for a given account.
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Box, Arc)]
|
||||
pub trait StorageRootProvider {
|
||||
/// Returns the storage root of the `HashedStorage` for target address on top of the current
|
||||
/// state.
|
||||
@@ -66,7 +66,7 @@ pub trait StorageRootProvider {
|
||||
}
|
||||
|
||||
/// A type that can generate state proof on top of a given post state.
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Box, Arc)]
|
||||
pub trait StateProofProvider {
|
||||
/// Get account and storage proofs of target keys in the `HashedPostState`
|
||||
/// on top of the current state.
|
||||
@@ -90,7 +90,7 @@ pub trait StateProofProvider {
|
||||
}
|
||||
|
||||
/// Trie Writer
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait TrieWriter: Send {
|
||||
/// Writes trie updates to the database.
|
||||
///
|
||||
@@ -106,7 +106,7 @@ pub trait TrieWriter: Send {
|
||||
}
|
||||
|
||||
/// Storage Trie Writer
|
||||
#[auto_impl::auto_impl(&, Box)]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait StorageTrieWriter: Send {
|
||||
/// Writes storage trie updates from the given storage trie map with already sorted updates.
|
||||
///
|
||||
|
||||
@@ -29,11 +29,12 @@ dyn-clone.workspace = true
|
||||
|
||||
# feature `rayon`
|
||||
rayon = { workspace = true, optional = true }
|
||||
crossbeam-utils = { workspace = true, optional = true }
|
||||
pin-project = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time", "macros"] }
|
||||
|
||||
[features]
|
||||
rayon = ["dep:rayon", "pin-project"]
|
||||
rayon = ["dep:rayon", "dep:crossbeam-utils", "pin-project"]
|
||||
test-utils = []
|
||||
|
||||
298
crates/tasks/src/for_each_ordered.rs
Normal file
298
crates/tasks/src/for_each_ordered.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
use crossbeam_utils::CachePadded;
|
||||
use rayon::iter::{IndexedParallelIterator, ParallelIterator};
|
||||
use std::{
|
||||
cell::UnsafeCell,
|
||||
mem::MaybeUninit,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
/// Extension trait for [`IndexedParallelIterator`]
|
||||
/// that streams results to a sequential consumer in index order.
|
||||
pub trait ForEachOrdered: IndexedParallelIterator {
|
||||
/// Executes the parallel iterator, calling `f` on each result **sequentially in index
|
||||
/// order**.
|
||||
///
|
||||
/// Items are computed in parallel, but `f` is invoked as `f(item_0)`, `f(item_1)`, …,
|
||||
/// `f(item_{n-1})` on the calling thread. The calling thread receives each item as soon
|
||||
/// as it (and all preceding items) are ready.
|
||||
///
|
||||
/// `f` does **not** need to be [`Send`] — it runs exclusively on the calling thread.
|
||||
fn for_each_ordered<F>(self, f: F)
|
||||
where
|
||||
Self::Item: Send,
|
||||
F: FnMut(Self::Item);
|
||||
}
|
||||
|
||||
impl<I: IndexedParallelIterator> ForEachOrdered for I {
|
||||
fn for_each_ordered<F>(self, f: F)
|
||||
where
|
||||
Self::Item: Send,
|
||||
F: FnMut(Self::Item),
|
||||
{
|
||||
ordered_impl(self, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache-line-padded slot with an atomic ready flag.
|
||||
struct Slot<T> {
|
||||
value: UnsafeCell<MaybeUninit<T>>,
|
||||
ready: AtomicBool,
|
||||
}
|
||||
|
||||
// SAFETY: Each slot is written by exactly one producer and read by exactly one consumer.
|
||||
// The AtomicBool synchronizes access (Release on write, Acquire on read).
|
||||
unsafe impl<T: Send> Send for Slot<T> {}
|
||||
unsafe impl<T: Send> Sync for Slot<T> {}
|
||||
|
||||
struct Shared<T> {
|
||||
slots: Box<[CachePadded<Slot<T>>]>,
|
||||
panicked: AtomicBool,
|
||||
}
|
||||
|
||||
impl<T> Shared<T> {
|
||||
fn new(n: usize) -> Self {
|
||||
Self {
|
||||
// SAFETY: Zero is a valid bit pattern for Slot.
|
||||
// Needs to be zero for `ready` to be false.
|
||||
slots: unsafe {
|
||||
Box::<[_]>::assume_init(Box::<[CachePadded<Slot<T>>]>::new_zeroed_slice(n))
|
||||
},
|
||||
panicked: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Index `i` must be in bounds and must only be written once.
|
||||
#[inline]
|
||||
unsafe fn write(&self, i: usize, val: T) {
|
||||
let slot = unsafe { self.slots.get_unchecked(i) };
|
||||
unsafe { (*slot.value.get()).write(val) };
|
||||
slot.ready.store(true, Ordering::Release);
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Index `i` must be in bounds. Must only be called after `ready` is observed `true`.
|
||||
#[inline]
|
||||
unsafe fn take(&self, i: usize) -> T {
|
||||
let slot = unsafe { self.slots.get_unchecked(i) };
|
||||
let v = unsafe { (*slot.value.get()).assume_init_read() };
|
||||
// Clear ready so Drop doesn't double-free this slot.
|
||||
slot.ready.store(false, Ordering::Relaxed);
|
||||
v
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_ready(&self, i: usize) -> bool {
|
||||
// SAFETY: caller ensures `i < n`.
|
||||
unsafe { self.slots.get_unchecked(i) }.ready.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for Shared<T> {
|
||||
fn drop(&mut self) {
|
||||
for slot in &mut *self.slots {
|
||||
if *slot.ready.get_mut() {
|
||||
unsafe { (*slot.value.get()).assume_init_drop() };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a parallel iterator and delivers results to a sequential callback in index order.
|
||||
///
|
||||
/// This works by pre-allocating one cache-line-padded slot per item. Each slot holds an
|
||||
/// `UnsafeCell<MaybeUninit<T>>` and an `AtomicBool` ready flag. A rayon task computes all
|
||||
/// items in parallel, writing each result into its slot and setting the flag (`Release`).
|
||||
/// The calling thread walks slots 0, 1, 2, … in order, spinning on the flag (`Acquire`),
|
||||
/// then reading the value and passing it to `f`.
|
||||
fn ordered_impl<I, F>(iter: I, mut f: F)
|
||||
where
|
||||
I: IndexedParallelIterator,
|
||||
I::Item: Send,
|
||||
F: FnMut(I::Item),
|
||||
{
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
|
||||
let n = iter.len();
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let shared = Shared::<I::Item>::new(n);
|
||||
|
||||
rayon::in_place_scope(|s| {
|
||||
// Producer: compute items in parallel and write them into their slots.
|
||||
s.spawn(|_| {
|
||||
let res = catch_unwind(AssertUnwindSafe(|| {
|
||||
iter.enumerate().for_each(|(i, item)| {
|
||||
// SAFETY: `enumerate()` on an IndexedParallelIterator yields each
|
||||
// index exactly once.
|
||||
unsafe { shared.write(i, item) };
|
||||
});
|
||||
}));
|
||||
if let Err(payload) = res {
|
||||
shared.panicked.store(true, Ordering::Release);
|
||||
std::panic::resume_unwind(payload);
|
||||
}
|
||||
});
|
||||
|
||||
// Consumer: sequential, ordered, on the calling thread.
|
||||
// Exponential backoff: 1, 2, 4, …, 64 pause instructions, then OS yields.
|
||||
const SPIN_SHIFT_LIMIT: u32 = 6;
|
||||
for i in 0..n {
|
||||
let mut backoff = 0u32;
|
||||
'wait: loop {
|
||||
if shared.is_ready(i) {
|
||||
break 'wait;
|
||||
}
|
||||
|
||||
if shared.panicked.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Yield to rayon's work-stealing so the producer can make progress,
|
||||
// especially important when the thread pool is small.
|
||||
if rayon::yield_now() == Some(rayon::Yield::Executed) {
|
||||
continue 'wait;
|
||||
}
|
||||
|
||||
if backoff < SPIN_SHIFT_LIMIT {
|
||||
for _ in 0..(1u32 << backoff) {
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
backoff += 1;
|
||||
} else {
|
||||
// Producer is genuinely slow; fall back to OS-level yield.
|
||||
std::thread::yield_now();
|
||||
}
|
||||
}
|
||||
// SAFETY: `i < n` and we just observed the ready flag with Acquire ordering.
|
||||
let value = unsafe { shared.take(i) };
|
||||
f(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rayon::prelude::*;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Barrier,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn preserves_order() {
|
||||
let input: Vec<u64> = (0..1000).collect();
|
||||
let mut output = Vec::with_capacity(input.len());
|
||||
input.par_iter().map(|x| x * 2).for_each_ordered(|x| output.push(x));
|
||||
let expected: Vec<u64> = (0..1000).map(|x| x * 2).collect();
|
||||
assert_eq!(output, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_iterator() {
|
||||
let input: Vec<u64> = vec![];
|
||||
let mut output = Vec::new();
|
||||
input.par_iter().map(|x| *x).for_each_ordered(|x| output.push(x));
|
||||
assert!(output.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_element() {
|
||||
let mut output = Vec::new();
|
||||
vec![42u64].par_iter().map(|x| *x).for_each_ordered(|x| output.push(x));
|
||||
assert_eq!(output, vec![42]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slow_early_items_still_delivered_in_order() {
|
||||
// Item 0 is deliberately delayed; all other items complete quickly.
|
||||
// The consumer must still deliver items in order 0, 1, 2, … regardless.
|
||||
let barrier = Barrier::new(2);
|
||||
let n = 64usize;
|
||||
let input: Vec<usize> = (0..n).collect();
|
||||
let mut output = Vec::with_capacity(n);
|
||||
|
||||
input
|
||||
.par_iter()
|
||||
.map(|&i| {
|
||||
if i == 0 {
|
||||
// Wait until at least one other item has been produced.
|
||||
barrier.wait();
|
||||
} else if i == n - 1 {
|
||||
// Signal that other items are ready.
|
||||
barrier.wait();
|
||||
}
|
||||
i
|
||||
})
|
||||
.for_each_ordered(|x| output.push(x));
|
||||
|
||||
assert_eq!(output, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_unconsumed_slots_on_panic() {
|
||||
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Tracked(#[allow(dead_code)] u64);
|
||||
impl Drop for Tracked {
|
||||
fn drop(&mut self) {
|
||||
DROP_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
DROP_COUNT.store(0, Ordering::Relaxed);
|
||||
|
||||
let input: Vec<u64> = (0..100).collect();
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
input
|
||||
.par_iter()
|
||||
.map(|&i| {
|
||||
assert!(i != 50, "intentional");
|
||||
Tracked(i)
|
||||
})
|
||||
.for_each_ordered(|_item| {});
|
||||
});
|
||||
|
||||
assert!(result.is_err());
|
||||
// All produced Tracked values must have been dropped (either consumed or cleaned up).
|
||||
// We can't assert an exact count since the panic may cut production short.
|
||||
let drops = DROP_COUNT.load(Ordering::Relaxed);
|
||||
assert!(drops > 0, "some items should have been dropped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_double_drop() {
|
||||
// Verify that consumed items are dropped exactly once (not double-freed by Drop).
|
||||
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
struct Counted(#[allow(dead_code)] u64);
|
||||
impl Drop for Counted {
|
||||
fn drop(&mut self) {
|
||||
DROP_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
DROP_COUNT.store(0, Ordering::Relaxed);
|
||||
let n = 200u64;
|
||||
let input: Vec<u64> = (0..n).collect();
|
||||
input.par_iter().map(|&i| Counted(i)).for_each_ordered(|_item| {});
|
||||
|
||||
assert_eq!(DROP_COUNT.load(Ordering::Relaxed), n as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_is_not_send() {
|
||||
// Verify that the callback does not need to be Send.
|
||||
use std::rc::Rc;
|
||||
let counter = Rc::new(std::cell::Cell::new(0u64));
|
||||
let input: Vec<u64> = (0..100).collect();
|
||||
input.par_iter().map(|&x| x).for_each_ordered(|x| {
|
||||
counter.set(counter.get() + x);
|
||||
});
|
||||
assert_eq!(counter.get(), (0..100u64).sum::<u64>());
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,12 @@ pub mod shutdown;
|
||||
#[cfg(feature = "rayon")]
|
||||
pub mod pool;
|
||||
|
||||
/// Lock-free ordered parallel iterator extension trait.
|
||||
#[cfg(feature = "rayon")]
|
||||
pub mod for_each_ordered;
|
||||
#[cfg(feature = "rayon")]
|
||||
pub use for_each_ordered::ForEachOrdered;
|
||||
|
||||
#[cfg(feature = "rayon")]
|
||||
pub use runtime::RayonConfig;
|
||||
pub use runtime::{Runtime, RuntimeBuildError, RuntimeBuilder, RuntimeConfig, TokioConfig};
|
||||
|
||||
@@ -110,6 +110,9 @@ pub struct RayonConfig {
|
||||
/// Number of threads for the proof account worker pool (trie account proof workers).
|
||||
/// If `None`, derived from available parallelism.
|
||||
pub proof_account_worker_threads: Option<usize>,
|
||||
/// Number of threads for the prewarming pool (execution prewarming workers).
|
||||
/// If `None`, derived from available parallelism.
|
||||
pub prewarming_threads: Option<usize>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "rayon")]
|
||||
@@ -123,6 +126,7 @@ impl Default for RayonConfig {
|
||||
max_blocking_tasks: DEFAULT_MAX_BLOCKING_TASKS,
|
||||
proof_storage_worker_threads: None,
|
||||
proof_account_worker_threads: None,
|
||||
prewarming_threads: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,6 +175,12 @@ impl RayonConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the number of threads for the prewarming pool.
|
||||
pub const fn with_prewarming_threads(mut self, prewarming_threads: usize) -> Self {
|
||||
self.prewarming_threads = Some(prewarming_threads);
|
||||
self
|
||||
}
|
||||
|
||||
/// Compute the default number of threads based on available parallelism.
|
||||
fn default_thread_count(&self) -> usize {
|
||||
self.cpu_threads.unwrap_or_else(|| {
|
||||
@@ -260,6 +270,9 @@ struct RuntimeInner {
|
||||
/// Proof account worker pool (trie account proof computation).
|
||||
#[cfg(feature = "rayon")]
|
||||
proof_account_worker_pool: rayon::ThreadPool,
|
||||
/// Prewarming pool (execution prewarming workers).
|
||||
#[cfg(feature = "rayon")]
|
||||
prewarming_pool: rayon::ThreadPool,
|
||||
/// Handle to the spawned [`TaskManager`] background task.
|
||||
/// The task monitors critical tasks for panics and fires the shutdown signal.
|
||||
/// Can be taken via [`Runtime::take_task_manager_handle`] to poll for panic errors.
|
||||
@@ -355,6 +368,12 @@ impl Runtime {
|
||||
pub fn proof_account_worker_pool(&self) -> &rayon::ThreadPool {
|
||||
&self.0.proof_account_worker_pool
|
||||
}
|
||||
|
||||
/// Get the prewarming pool.
|
||||
#[cfg(feature = "rayon")]
|
||||
pub fn prewarming_pool(&self) -> &rayon::ThreadPool {
|
||||
&self.0.prewarming_pool
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test helpers ──────────────────────────────────────────────────────
|
||||
@@ -394,6 +413,7 @@ impl Runtime {
|
||||
max_blocking_tasks: 16,
|
||||
proof_storage_worker_threads: Some(2),
|
||||
proof_account_worker_threads: Some(2),
|
||||
prewarming_threads: Some(2),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -824,6 +844,7 @@ impl RuntimeBuilder {
|
||||
blocking_guard,
|
||||
proof_storage_worker_pool,
|
||||
proof_account_worker_pool,
|
||||
prewarming_pool,
|
||||
) = {
|
||||
let default_threads = config.rayon.default_thread_count();
|
||||
let rpc_threads = config.rayon.rpc_threads.unwrap_or(default_threads);
|
||||
@@ -862,12 +883,19 @@ impl RuntimeBuilder {
|
||||
.thread_name(|i| format!("proof-acct-{i:02}"))
|
||||
.build()?;
|
||||
|
||||
let prewarming_threads = config.rayon.prewarming_threads.unwrap_or(default_threads);
|
||||
let prewarming_pool = rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(prewarming_threads)
|
||||
.thread_name(|i| format!("prewarm-{i:02}"))
|
||||
.build()?;
|
||||
|
||||
debug!(
|
||||
default_threads,
|
||||
rpc_threads,
|
||||
storage_threads,
|
||||
proof_storage_worker_threads,
|
||||
proof_account_worker_threads,
|
||||
prewarming_threads,
|
||||
max_blocking_tasks = config.rayon.max_blocking_tasks,
|
||||
"Initialized rayon thread pools"
|
||||
);
|
||||
@@ -879,6 +907,7 @@ impl RuntimeBuilder {
|
||||
blocking_guard,
|
||||
proof_storage_worker_pool,
|
||||
proof_account_worker_pool,
|
||||
prewarming_pool,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -909,6 +938,8 @@ impl RuntimeBuilder {
|
||||
proof_storage_worker_pool,
|
||||
#[cfg(feature = "rayon")]
|
||||
proof_account_worker_pool,
|
||||
#[cfg(feature = "rayon")]
|
||||
prewarming_pool,
|
||||
task_manager_handle: Mutex::new(Some(task_manager_handle)),
|
||||
};
|
||||
|
||||
|
||||
@@ -501,11 +501,26 @@ where
|
||||
results.pop().expect("result length is the same as the input")
|
||||
}
|
||||
|
||||
async fn add_transactions(
|
||||
&self,
|
||||
origin: TransactionOrigin,
|
||||
transactions: Vec<Self::Transaction>,
|
||||
) -> Vec<PoolResult<AddedTransactionOutcome>> {
|
||||
if transactions.is_empty() {
|
||||
return Vec::new()
|
||||
}
|
||||
let validated = self
|
||||
.pool
|
||||
.validator()
|
||||
.validate_transactions(transactions.into_iter().map(|tx| (origin, tx)))
|
||||
.await;
|
||||
self.pool.add_transactions(origin, validated)
|
||||
}
|
||||
|
||||
async fn add_transactions_with_origins(
|
||||
&self,
|
||||
transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction)> + Send,
|
||||
transactions: Vec<(TransactionOrigin, Self::Transaction)>,
|
||||
) -> Vec<PoolResult<AddedTransactionOutcome>> {
|
||||
let transactions: Vec<_> = transactions.into_iter().collect();
|
||||
if transactions.is_empty() {
|
||||
return Vec::new()
|
||||
}
|
||||
|
||||
@@ -84,9 +84,23 @@ impl<T: EthPoolTransaction> TransactionPool for NoopTransactionPool<T> {
|
||||
Err(PoolError::other(hash, Box::new(NoopInsertError::new(transaction))))
|
||||
}
|
||||
|
||||
async fn add_transactions(
|
||||
&self,
|
||||
_origin: TransactionOrigin,
|
||||
transactions: Vec<Self::Transaction>,
|
||||
) -> Vec<PoolResult<AddedTransactionOutcome>> {
|
||||
transactions
|
||||
.into_iter()
|
||||
.map(|transaction| {
|
||||
let hash = *transaction.hash();
|
||||
Err(PoolError::other(hash, Box::new(NoopInsertError::new(transaction))))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn add_transactions_with_origins(
|
||||
&self,
|
||||
transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction)> + Send,
|
||||
transactions: Vec<(TransactionOrigin, Self::Transaction)>,
|
||||
) -> Vec<PoolResult<AddedTransactionOutcome>> {
|
||||
transactions
|
||||
.into_iter()
|
||||
|
||||
@@ -704,6 +704,10 @@ impl PoolTransaction for MockTransaction {
|
||||
|
||||
type Pooled = PooledTransactionVariant;
|
||||
|
||||
fn consensus_ref(&self) -> Recovered<&Self::Consensus> {
|
||||
unimplemented!("mock transaction does not wrap a consensus transaction")
|
||||
}
|
||||
|
||||
fn into_consensus(self) -> Recovered<Self::Consensus> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
@@ -175,9 +175,7 @@ pub trait TransactionPool: Clone + Debug + Send + Sync {
|
||||
&self,
|
||||
origin: TransactionOrigin,
|
||||
transactions: Vec<Self::Transaction>,
|
||||
) -> impl Future<Output = Vec<PoolResult<AddedTransactionOutcome>>> + Send {
|
||||
self.add_transactions_with_origins(transactions.into_iter().map(move |tx| (origin, tx)))
|
||||
}
|
||||
) -> impl Future<Output = Vec<PoolResult<AddedTransactionOutcome>>> + Send;
|
||||
|
||||
/// Adds the given _unvalidated_ transactions into the pool.
|
||||
///
|
||||
@@ -188,7 +186,7 @@ pub trait TransactionPool: Clone + Debug + Send + Sync {
|
||||
/// Consumer: RPC
|
||||
fn add_transactions_with_origins(
|
||||
&self,
|
||||
transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction)> + Send,
|
||||
transactions: Vec<(TransactionOrigin, Self::Transaction)>,
|
||||
) -> impl Future<Output = Vec<PoolResult<AddedTransactionOutcome>>> + Send;
|
||||
|
||||
/// Submit a consensus transaction directly to the pool
|
||||
@@ -1259,6 +1257,9 @@ pub trait PoolTransaction:
|
||||
self.clone().into_consensus()
|
||||
}
|
||||
|
||||
/// Returns a reference to the consensus transaction with the recovered sender.
|
||||
fn consensus_ref(&self) -> Recovered<&Self::Consensus>;
|
||||
|
||||
/// Define a method to convert from the `Self` type to `Consensus`
|
||||
fn into_consensus(self) -> Recovered<Self::Consensus>;
|
||||
|
||||
@@ -1449,6 +1450,10 @@ impl PoolTransaction for EthPooledTransaction {
|
||||
self.transaction().clone()
|
||||
}
|
||||
|
||||
fn consensus_ref(&self) -> Recovered<&Self::Consensus> {
|
||||
Recovered::new_unchecked(&*self.transaction, self.transaction.signer())
|
||||
}
|
||||
|
||||
fn into_consensus(self) -> Recovered<Self::Consensus> {
|
||||
self.transaction
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ mod tests {
|
||||
reth_provider::providers::OverlayStateProviderFactory::new(factory, changeset_cache);
|
||||
let task_ctx = ProofTaskCtx::new(factory);
|
||||
let runtime = reth_tasks::Runtime::test();
|
||||
let proof_worker_handle = ProofWorkerHandle::new(&runtime, task_ctx, false);
|
||||
let proof_worker_handle = ProofWorkerHandle::new(&runtime, task_ctx, false, false);
|
||||
|
||||
let parallel_result = ParallelProof::new(Default::default(), proof_worker_handle.clone())
|
||||
.decoded_multiproof(targets.clone())
|
||||
|
||||
@@ -75,7 +75,7 @@ use std::{
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::{debug, debug_span, error, trace};
|
||||
use tracing::{debug, debug_span, error, instrument, trace};
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
use crate::proof_task_metrics::{
|
||||
@@ -134,10 +134,18 @@ impl ProofWorkerHandle {
|
||||
/// # Parameters
|
||||
/// - `runtime`: The centralized runtime used to spawn blocking worker tasks
|
||||
/// - `task_ctx`: Shared context with database view and prefix sets
|
||||
/// - `halve_workers`: Whether to halve the worker pool size (for small blocks)
|
||||
/// - `v2_proofs_enabled`: Whether to enable V2 storage proofs
|
||||
#[instrument(
|
||||
name = "ProofWorkerHandle::new",
|
||||
level = "debug",
|
||||
target = "trie::proof_task",
|
||||
skip_all
|
||||
)]
|
||||
pub fn new<Factory>(
|
||||
runtime: &Runtime,
|
||||
task_ctx: ProofTaskCtx<Factory>,
|
||||
halve_workers: bool,
|
||||
v2_proofs_enabled: bool,
|
||||
) -> Self
|
||||
where
|
||||
@@ -154,13 +162,17 @@ impl ProofWorkerHandle {
|
||||
|
||||
let cached_storage_roots = Arc::<DashMap<_, _>>::default();
|
||||
|
||||
let storage_worker_count = runtime.proof_storage_worker_pool().current_num_threads();
|
||||
let account_worker_count = runtime.proof_account_worker_pool().current_num_threads();
|
||||
let divisor = if halve_workers { 2 } else { 1 };
|
||||
let storage_worker_count =
|
||||
runtime.proof_storage_worker_pool().current_num_threads() / divisor;
|
||||
let account_worker_count =
|
||||
runtime.proof_account_worker_pool().current_num_threads() / divisor;
|
||||
|
||||
debug!(
|
||||
target: "trie::proof_task",
|
||||
storage_worker_count,
|
||||
account_worker_count,
|
||||
halve_workers,
|
||||
?v2_proofs_enabled,
|
||||
"Spawning proof worker pools"
|
||||
);
|
||||
@@ -2012,7 +2024,7 @@ mod tests {
|
||||
let ctx = test_ctx(factory);
|
||||
|
||||
let runtime = reth_tasks::Runtime::test();
|
||||
let proof_handle = ProofWorkerHandle::new(&runtime, ctx, false);
|
||||
let proof_handle = ProofWorkerHandle::new(&runtime, ctx, false, false);
|
||||
|
||||
// Verify handle can be cloned
|
||||
let _cloned_handle = proof_handle.clone();
|
||||
|
||||
@@ -1159,7 +1159,7 @@ where
|
||||
name = "SparseStateTrie::prune",
|
||||
target = "trie::sparse",
|
||||
skip_all,
|
||||
fields(max_depth, max_storage_tries)
|
||||
fields(%max_depth, %max_storage_tries)
|
||||
)]
|
||||
pub fn prune(&mut self, max_depth: usize, max_storage_tries: usize) {
|
||||
// Prune state and storage tries in parallel
|
||||
|
||||
@@ -981,6 +981,9 @@ Engine:
|
||||
--engine.account-worker-count <ACCOUNT_WORKER_COUNT>
|
||||
Configure the number of account proof workers in the Tokio blocking pool. If not specified, defaults to the same count as storage workers
|
||||
|
||||
--engine.prewarming-threads <PREWARMING_THREADS>
|
||||
Configure the number of prewarming threads. If not specified, defaults to available parallelism
|
||||
|
||||
--engine.disable-proof-v2
|
||||
Disable V2 storage proofs for state root calculations
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ async fn main() -> eyre::Result<()> {
|
||||
IncomingEthRequest::GetReceipts { .. } => {}
|
||||
IncomingEthRequest::GetReceipts69 { .. } => {}
|
||||
IncomingEthRequest::GetReceipts70 { .. } => {}
|
||||
IncomingEthRequest::GetBlockAccessLists { .. } => {}
|
||||
}
|
||||
}
|
||||
transaction_message = transactions_rx.recv() => {
|
||||
|
||||
Reference in New Issue
Block a user