mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
11 Commits
dani/docke
...
devnet4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b2c458302 | ||
|
|
0722202930 | ||
|
|
8ec6e614f9 | ||
|
|
2c86c0b876 | ||
|
|
bd4cd28a8d | ||
|
|
6fa48a497a | ||
|
|
6886cd7742 | ||
|
|
eeb223f0b8 | ||
|
|
f344f5abfb | ||
|
|
68845d1114 | ||
|
|
ecfb6cc089 |
2
.github/scripts/bench-reth-build.sh
vendored
2
.github/scripts/bench-reth-build.sh
vendored
@@ -53,7 +53,7 @@ build_node_binary() {
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
|
||||
cargo build --profile profiling $NODE_PKG $workspace_arg $features_arg
|
||||
cargo build --locked --profile profiling $NODE_PKG $workspace_arg $features_arg
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
|
||||
29
.github/workflows/bench-scheduled.yml
vendored
29
.github/workflows/bench-scheduled.yml
vendored
@@ -366,19 +366,24 @@ jobs:
|
||||
|
||||
- name: Prepare source dirs
|
||||
run: |
|
||||
if [ -d ../reth-baseline ]; then
|
||||
git -C ../reth-baseline fetch origin "$BASELINE_REF"
|
||||
else
|
||||
git clone . ../reth-baseline
|
||||
fi
|
||||
git -C ../reth-baseline checkout "$BASELINE_REF"
|
||||
prepare_source_dir() {
|
||||
local dir="$1"
|
||||
local ref="$2"
|
||||
|
||||
if [ -d ../reth-feature ]; then
|
||||
git -C ../reth-feature fetch origin "$FEATURE_REF"
|
||||
else
|
||||
git clone . ../reth-feature
|
||||
fi
|
||||
git -C ../reth-feature checkout "$FEATURE_REF"
|
||||
if [ -d "$dir" ]; then
|
||||
git -C "$dir" reset --hard HEAD
|
||||
git -C "$dir" clean -fdx
|
||||
git -C "$dir" fetch origin "$ref"
|
||||
else
|
||||
git clone . "$dir"
|
||||
fi
|
||||
|
||||
git -C "$dir" checkout --force "$ref"
|
||||
}
|
||||
|
||||
prepare_source_dir ../reth-baseline "$BASELINE_REF"
|
||||
|
||||
prepare_source_dir ../reth-feature "$FEATURE_REF"
|
||||
|
||||
- name: Build binaries
|
||||
id: build
|
||||
|
||||
29
.github/workflows/bench.yml
vendored
29
.github/workflows/bench.yml
vendored
@@ -802,21 +802,26 @@ jobs:
|
||||
|
||||
- name: Prepare source dirs
|
||||
run: |
|
||||
prepare_source_dir() {
|
||||
local dir="$1"
|
||||
local ref="$2"
|
||||
|
||||
if [ -d "$dir" ]; then
|
||||
git -C "$dir" reset --hard HEAD
|
||||
git -C "$dir" clean -fdx
|
||||
git -C "$dir" fetch origin "$ref"
|
||||
else
|
||||
git clone . "$dir"
|
||||
fi
|
||||
|
||||
git -C "$dir" checkout --force "$ref"
|
||||
}
|
||||
|
||||
BASELINE_REF="${{ steps.refs.outputs.baseline-ref }}"
|
||||
if [ -d ../reth-baseline ]; then
|
||||
git -C ../reth-baseline fetch origin "$BASELINE_REF"
|
||||
else
|
||||
git clone . ../reth-baseline
|
||||
fi
|
||||
git -C ../reth-baseline checkout "$BASELINE_REF"
|
||||
prepare_source_dir ../reth-baseline "$BASELINE_REF"
|
||||
|
||||
FEATURE_REF="${{ steps.refs.outputs.feature-ref }}"
|
||||
if [ -d ../reth-feature ]; then
|
||||
git -C ../reth-feature fetch origin "$FEATURE_REF"
|
||||
else
|
||||
git clone . ../reth-feature
|
||||
fi
|
||||
git -C ../reth-feature checkout "$FEATURE_REF"
|
||||
prepare_source_dir ../reth-feature "$FEATURE_REF"
|
||||
|
||||
- name: Build binaries
|
||||
id: build
|
||||
|
||||
385
Cargo.lock
generated
385
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
37
Cargo.toml
37
Cargo.toml
@@ -433,14 +433,14 @@ reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
|
||||
reth-zstd-compressors = { version = "0.3.1", default-features = false }
|
||||
|
||||
# revm
|
||||
revm = { version = "38.0.0", default-features = false }
|
||||
revm-bytecode = { version = "10.0.0", default-features = false }
|
||||
revm-database = { version = "13.0.0", default-features = false }
|
||||
revm-state = { version = "11.0.0", default-features = false }
|
||||
revm-primitives = { version = "23.0.0", default-features = false }
|
||||
revm-interpreter = { version = "35.0.0", default-features = false }
|
||||
revm-database-interface = { version = "11.0.0", default-features = false }
|
||||
revm-inspectors = "0.39.0"
|
||||
revm = { version = "=37.0.0", default-features = false }
|
||||
revm-bytecode = { version = "=10.0.0", default-features = false }
|
||||
revm-database = { version = "=13.0.0", default-features = false }
|
||||
revm-state = { version = "=11.0.0", default-features = false }
|
||||
revm-primitives = { version = "=23.0.0", default-features = false }
|
||||
revm-interpreter = { version = "=35.0.0", default-features = false }
|
||||
revm-database-interface = { version = "=11.0.0", default-features = false }
|
||||
revm-inspectors = "=0.39.0"
|
||||
|
||||
# eth
|
||||
alloy-dyn-abi = "1.5.6"
|
||||
@@ -700,3 +700,24 @@ vergen-git2 = "9.1.0"
|
||||
|
||||
# networking
|
||||
ipnet = "2.11"
|
||||
|
||||
[patch.crates-io]
|
||||
revm = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-context = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-database = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-state = { git = "https://github.com/bluealloy/revm", rev = "fe2549d85fb9e201e7b629f8b47bcca46d49aa1d" }
|
||||
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "a2c7a41977b468d016a339f560acb76e002766f3" }
|
||||
alloy-evm = { git = "https://github.com/alloy-rs/evm", rev = "da7633f6bc9554f5a6e60773ef21b8e9d6e0cca6" }
|
||||
reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "c763480b9fa51957fbdb69b7caead5dfc4e3752c" }
|
||||
|
||||
@@ -69,6 +69,7 @@ default = [
|
||||
"jemalloc",
|
||||
"reth-cli-util/jemalloc",
|
||||
"asm-keccak",
|
||||
"keccak-cache-global",
|
||||
"min-debug-logs",
|
||||
]
|
||||
|
||||
@@ -89,6 +90,12 @@ asm-keccak = [
|
||||
"revm-primitives/asm-keccak",
|
||||
]
|
||||
|
||||
keccak-cache-global = [
|
||||
"reth-node-core/keccak-cache-global",
|
||||
"reth-node-ethereum/keccak-cache-global",
|
||||
"alloy-primitives/keccak-cache-global",
|
||||
]
|
||||
|
||||
min-debug-logs = [
|
||||
"tracing/release_max_level_debug",
|
||||
"reth-ethereum-cli/min-debug-logs",
|
||||
|
||||
@@ -50,8 +50,13 @@ where
|
||||
info!(target: "reth::cli", new_tip = ?header.num_hash(), "Setting up dummy EVM chain before importing state.");
|
||||
|
||||
let static_file_provider = provider_rw.static_file_provider();
|
||||
// Write EVM dummy data up to `header - 1` block
|
||||
append_dummy_chain(&static_file_provider, header.number() - 1, header_factory)?;
|
||||
// Write EVM dummy data up to `header - 1` block. Skip when the supplied
|
||||
// header is at block 0: `header.number() - 1` would underflow in u64 to
|
||||
// `u64::MAX`, sending `append_dummy_chain` into a 1..=u64::MAX loop that
|
||||
// exhausts memory before failing.
|
||||
if header.number() > 0 {
|
||||
append_dummy_chain(&static_file_provider, header.number() - 1, header_factory)?;
|
||||
}
|
||||
|
||||
info!(target: "reth::cli", "Appending first valid block.");
|
||||
|
||||
@@ -191,7 +196,13 @@ mod tests {
|
||||
use alloy_primitives::{address, b256};
|
||||
use reth_db_common::init::init_genesis;
|
||||
use reth_provider::{test_utils::create_test_provider_factory, DatabaseProviderFactory};
|
||||
use std::io::Write;
|
||||
use std::{
|
||||
io::Write,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
@@ -264,4 +275,45 @@ mod tests {
|
||||
|
||||
assert_eq!(actual_next_height, expected_next_height);
|
||||
}
|
||||
|
||||
/// Regression: a header at block 0 used to send `append_dummy_chain` into
|
||||
/// a `1..=u64::MAX` loop because `header.number() - 1` underflowed in
|
||||
/// u64. The guard `if header.number() > 0` skips the dummy-chain step
|
||||
/// when there is no pre-genesis range to backfill, so `header_factory`
|
||||
/// is never invoked.
|
||||
#[test]
|
||||
fn test_setup_without_evm_skips_dummy_chain_for_genesis_header() {
|
||||
let header = Header { number: 0, ..Default::default() };
|
||||
let header_hash = header.hash_slow();
|
||||
|
||||
let provider_factory = create_test_provider_factory();
|
||||
init_genesis(&provider_factory).unwrap();
|
||||
let provider_rw = provider_factory.database_provider_rw().unwrap();
|
||||
|
||||
let factory_calls = Arc::new(AtomicU64::new(0));
|
||||
let factory_calls_inner = Arc::clone(&factory_calls);
|
||||
|
||||
// The Result of `setup_without_evm` itself is not asserted: with
|
||||
// `number == 0` plus a genesis already written by `init_genesis`,
|
||||
// the subsequent `append_first_block` may legitimately fail. The
|
||||
// bug under test is the OOM in the dummy-chain loop, observable
|
||||
// through the factory-call counter below.
|
||||
let _ = setup_without_evm(
|
||||
&provider_rw,
|
||||
SealedHeader::new(header, header_hash),
|
||||
move |number| {
|
||||
// Bound calls so a regression cannot exhaust the test
|
||||
// runner's memory; the only correct value here is 0.
|
||||
let n = factory_calls_inner.fetch_add(1, Ordering::Relaxed);
|
||||
assert!(n < 8, "header_factory must not be invoked for a genesis-block header");
|
||||
Header { number, ..Default::default() }
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
factory_calls.load(Ordering::Relaxed),
|
||||
0,
|
||||
"append_dummy_chain must be skipped when header.number() == 0"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::common::{
|
||||
EnvironmentArgs,
|
||||
};
|
||||
use alloy_consensus::{transaction::TxHashRef, BlockHeader, TxReceipt};
|
||||
use alloy_primitives::{Address, B256, U256};
|
||||
use clap::Parser;
|
||||
use eyre::WrapErr;
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks};
|
||||
@@ -12,15 +13,19 @@ use reth_cli::chainspec::ChainSpecParser;
|
||||
use reth_cli_util::cancellation::CancellationToken;
|
||||
use reth_consensus::FullConsensus;
|
||||
use reth_evm::{execute::Executor, ConfigureEvm};
|
||||
use reth_primitives_traits::{format_gas_throughput, BlockBody, GotExpected};
|
||||
use reth_primitives_traits::{format_gas_throughput, Account, BlockBody, GotExpected};
|
||||
use reth_provider::{
|
||||
BlockNumReader, BlockReader, ChainSpecProvider, DatabaseProviderFactory, ReceiptProvider,
|
||||
StaticFileProviderFactory, TransactionVariant,
|
||||
};
|
||||
use reth_revm::database::StateProviderDatabase;
|
||||
use reth_revm::{
|
||||
database::StateProviderDatabase,
|
||||
db::{states::reverts::AccountInfoRevert, BundleState},
|
||||
};
|
||||
use reth_stages::stages::calculate_gas_used_from_headers;
|
||||
use reth_storage_api::DBProvider;
|
||||
use reth_storage_api::{ChangeSetReader, DBProvider, StorageChangeSetReader};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
@@ -255,11 +260,28 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
if executor.size_hint() > 5_000_000 ||
|
||||
executor_created.elapsed() > executor_lifetime
|
||||
{
|
||||
executor =
|
||||
evm_config.batch_executor(db_at(block.number()));
|
||||
let last_block = block.number();
|
||||
let old_executor = std::mem::replace(
|
||||
&mut executor,
|
||||
evm_config.batch_executor(db_at(last_block)),
|
||||
);
|
||||
let bundle = old_executor.into_state().take_bundle();
|
||||
verify_bundle_against_changesets(
|
||||
&provider,
|
||||
&bundle,
|
||||
last_block,
|
||||
)?;
|
||||
executor_created = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
// Full verification at chunk end for remaining unverified blocks
|
||||
let bundle = executor.into_state().take_bundle();
|
||||
verify_bundle_against_changesets(
|
||||
&provider,
|
||||
&bundle,
|
||||
chunk_end - 1,
|
||||
)?;
|
||||
}
|
||||
|
||||
eyre::Ok(())
|
||||
@@ -340,3 +362,98 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies reverts against database changesets.
|
||||
///
|
||||
/// For each block, reverts must match changeset entries exactly. No extra slots/accounts
|
||||
/// in reverts for non-destroyed accounts. Destroyed accounts may have extra changeset slots
|
||||
/// (from DB storage wipe) absent from reverts.
|
||||
fn verify_bundle_against_changesets<P>(
|
||||
provider: &P,
|
||||
bundle: &BundleState,
|
||||
last_block: u64,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
P: ChangeSetReader + StorageChangeSetReader,
|
||||
{
|
||||
// Verify reverts against changesets per block
|
||||
for (i, block_reverts) in bundle.reverts.iter().rev().enumerate() {
|
||||
let block_number = last_block - i as u64;
|
||||
|
||||
let mut cs_accounts: HashMap<Address, Option<Account>> = provider
|
||||
.account_block_changeset(block_number)?
|
||||
.into_iter()
|
||||
.map(|cs| (cs.address, cs.info))
|
||||
.collect();
|
||||
|
||||
let mut cs_storage: HashMap<Address, HashMap<B256, U256>> = HashMap::new();
|
||||
for (bna, entry) in provider.storage_changeset(block_number)? {
|
||||
cs_storage.entry(bna.address()).or_default().insert(entry.key, entry.value);
|
||||
}
|
||||
|
||||
for (addr, revert) in block_reverts {
|
||||
// Verify account info
|
||||
match &revert.account {
|
||||
AccountInfoRevert::DoNothing => {
|
||||
eyre::ensure!(
|
||||
!cs_accounts.contains_key(addr),
|
||||
"Block {block_number}: account {addr} in changeset but revert is DoNothing",
|
||||
);
|
||||
}
|
||||
AccountInfoRevert::DeleteIt => {
|
||||
let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
|
||||
eyre::eyre!("Block {block_number}: account {addr} revert is DeleteIt but not in changeset")
|
||||
})?;
|
||||
eyre::ensure!(
|
||||
cs_info.is_none(),
|
||||
"Block {block_number}: account {addr} revert is DeleteIt but changeset has {cs_info:?}",
|
||||
);
|
||||
}
|
||||
AccountInfoRevert::RevertTo(info) => {
|
||||
let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
|
||||
eyre::eyre!("Block {block_number}: account {addr} revert is RevertTo but not in changeset")
|
||||
})?;
|
||||
let revert_acct = Some(Account::from(info));
|
||||
eyre::ensure!(
|
||||
revert_acct == cs_info,
|
||||
"Block {block_number}: account {addr} info mismatch: revert={revert_acct:?} cs={cs_info:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify storage slots — remove matched changeset entries as we go
|
||||
let mut cs_slots = cs_storage.get_mut(addr);
|
||||
for (slot_key, revert_slot) in &revert.storage {
|
||||
let b256_key = B256::from(*slot_key);
|
||||
match cs_slots.as_mut().and_then(|s| s.remove(&b256_key)) {
|
||||
Some(cs_value) => eyre::ensure!(
|
||||
revert_slot.to_previous_value() == cs_value,
|
||||
"Block {block_number}: {addr} slot {b256_key} mismatch: \
|
||||
revert={} cs={cs_value}",
|
||||
revert_slot.to_previous_value(),
|
||||
),
|
||||
None => eyre::ensure!(
|
||||
revert.wipe_storage,
|
||||
"Block {block_number}: {addr} slot {b256_key} in reverts but not in changeset",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining cs_storage slots for this address must be from a destroyed account
|
||||
if let Some(remaining) = cs_slots.filter(|s| !s.is_empty()) {
|
||||
eyre::ensure!(
|
||||
revert.wipe_storage,
|
||||
"Block {block_number}: {addr} has {} unmatched storage slots in changeset",
|
||||
remaining.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining cs_accounts entries had no corresponding revert
|
||||
if let Some(addr) = cs_accounts.keys().next() {
|
||||
eyre::bail!("Block {block_number}: account {addr} in changeset but not in reverts");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1159,19 +1159,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
let account = revm_state::Account {
|
||||
info: AccountInfo {
|
||||
balance: U256::from(rng.random::<u64>()),
|
||||
nonce: rng.random::<u64>(),
|
||||
code_hash: KECCAK_EMPTY,
|
||||
code: Some(Default::default()),
|
||||
account_id: None,
|
||||
},
|
||||
original_info: Box::new(AccountInfo::default()),
|
||||
storage,
|
||||
status: AccountStatus::Touched,
|
||||
transaction_id: 0,
|
||||
let mut account = revm_state::Account::default();
|
||||
account.info = AccountInfo {
|
||||
balance: U256::from(rng.random::<u64>()),
|
||||
nonce: rng.random::<u64>(),
|
||||
code_hash: KECCAK_EMPTY,
|
||||
code: Some(Default::default()),
|
||||
account_id: None,
|
||||
};
|
||||
account.storage = storage;
|
||||
account.status = AccountStatus::Touched;
|
||||
|
||||
state_update.insert(address, account);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
};
|
||||
use reth_eth_wire::{EthNetworkPrimitives, NetworkPrimitives};
|
||||
use reth_network_api::test_utils::PeersHandleProvider;
|
||||
use reth_storage_api::BalProvider;
|
||||
use reth_transaction_pool::TransactionPool;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
@@ -63,7 +64,10 @@ impl<Tx, Eth, N: NetworkPrimitives> NetworkBuilder<Tx, Eth, N> {
|
||||
pub fn request_handler<Client>(
|
||||
self,
|
||||
client: Client,
|
||||
) -> NetworkBuilder<Tx, EthRequestHandler<Client, N>, N> {
|
||||
) -> NetworkBuilder<Tx, EthRequestHandler<Client, N>, N>
|
||||
where
|
||||
Client: BalProvider,
|
||||
{
|
||||
let Self { mut network, transactions, .. } = self;
|
||||
let (tx, rx) = mpsc::channel(ETH_REQUEST_CHANNEL_CAPACITY);
|
||||
network.set_eth_request_handler(tx);
|
||||
|
||||
@@ -20,7 +20,9 @@ use reth_eth_wire_types::message::MAX_MESSAGE_SIZE;
|
||||
use reth_ethereum_forks::{ForkFilter, Head};
|
||||
use reth_network_peers::{mainnet_nodes, pk2id, sepolia_nodes, PeerId, TrustedPeer};
|
||||
use reth_network_types::{PeersConfig, SessionsConfig};
|
||||
use reth_storage_api::{noop::NoopProvider, BlockNumReader, BlockReader, HeaderProvider};
|
||||
use reth_storage_api::{
|
||||
noop::NoopProvider, BalProvider, BlockNumReader, BlockReader, HeaderProvider,
|
||||
};
|
||||
use reth_tasks::Runtime;
|
||||
use secp256k1::SECP256K1;
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
@@ -157,7 +159,8 @@ where
|
||||
impl<C, N> NetworkConfig<C, N>
|
||||
where
|
||||
N: NetworkPrimitives,
|
||||
C: BlockReader<Block = N::Block, Receipt = N::Receipt, Header = N::BlockHeader>
|
||||
C: BalProvider
|
||||
+ BlockReader<Block = N::Block, Receipt = N::Receipt, Header = N::BlockHeader>
|
||||
+ HeaderProvider
|
||||
+ Clone
|
||||
+ Unpin
|
||||
|
||||
@@ -18,7 +18,7 @@ use reth_network_api::test_utils::PeersHandle;
|
||||
use reth_network_p2p::error::RequestResult;
|
||||
use reth_network_peers::PeerId;
|
||||
use reth_primitives_traits::Block;
|
||||
use reth_storage_api::{BlockReader, HeaderProvider};
|
||||
use reth_storage_api::{BalProvider, BlockReader, GetBlockAccessListLimit, HeaderProvider};
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
@@ -282,27 +282,6 @@ where
|
||||
let _ = response.send(Ok(Receipts70 { last_block_incomplete, receipts }));
|
||||
}
|
||||
|
||||
/// Handles [`GetBlockAccessLists`] queries.
|
||||
///
|
||||
/// EIP-8159 defines the final `BlockAccessLists` response semantics:
|
||||
/// <https://eips.ethereum.org/EIPS/eip-8159>
|
||||
fn on_block_access_lists_request(
|
||||
&self,
|
||||
_peer_id: PeerId,
|
||||
request: GetBlockAccessLists,
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
) {
|
||||
// TODO: BAL serving is not fully implemented yet. Per EIP-8159, unavailable BALs are
|
||||
// returned as empty BAL entries while preserving request order, so we currently return
|
||||
// one RLP-encoded empty BAL (`0xc0`) per requested hash.
|
||||
let access_lists = request
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|_| Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]))
|
||||
.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
|
||||
@@ -332,13 +311,55 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, N> EthRequestHandler<C, N>
|
||||
where
|
||||
N: NetworkPrimitives,
|
||||
C: BalProvider,
|
||||
{
|
||||
/// Handles [`GetBlockAccessLists`] queries.
|
||||
///
|
||||
/// EIP-8159 defines the final `BlockAccessLists` response semantics:
|
||||
/// <https://eips.ethereum.org/EIPS/eip-8159>
|
||||
fn on_block_access_lists_request(
|
||||
&self,
|
||||
_peer_id: PeerId,
|
||||
request: GetBlockAccessLists,
|
||||
response: oneshot::Sender<RequestResult<BlockAccessLists>>,
|
||||
) {
|
||||
let limit = GetBlockAccessListLimit::ResponseSizeSoftLimit(SOFT_RESPONSE_LIMIT);
|
||||
let access_lists = self
|
||||
.client
|
||||
.bal_store()
|
||||
.get_by_hashes_with_limit(&request.0, limit)
|
||||
.unwrap_or_else(|_| empty_block_access_lists_with_limit(request.0.len(), limit));
|
||||
let _ = response.send(Ok(BlockAccessLists(access_lists)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the error fallback response while still enforcing the BAL response soft limit.
|
||||
fn empty_block_access_lists_with_limit(count: usize, limit: GetBlockAccessListLimit) -> Vec<Bytes> {
|
||||
let mut out = Vec::with_capacity(count);
|
||||
let mut size = 0;
|
||||
for _ in 0..count {
|
||||
let bal = Bytes::from_static(&[0xc0]);
|
||||
size += bal.len();
|
||||
out.push(bal);
|
||||
|
||||
if limit.exceeds(size) {
|
||||
break
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// An endless future.
|
||||
///
|
||||
/// This should be spawned or used as part of `tokio::select!`.
|
||||
impl<C, N> Future for EthRequestHandler<C, N>
|
||||
where
|
||||
N: NetworkPrimitives,
|
||||
C: BlockReader<Block = N::Block, Receipt = N::Receipt>
|
||||
C: BalProvider
|
||||
+ BlockReader<Block = N::Block, Receipt = N::Receipt>
|
||||
+ HeaderProvider<Header = N::BlockHeader>
|
||||
+ Unpin,
|
||||
{
|
||||
|
||||
@@ -27,7 +27,8 @@ use reth_network_api::{
|
||||
};
|
||||
use reth_network_peers::PeerId;
|
||||
use reth_storage_api::{
|
||||
noop::NoopProvider, BlockReader, BlockReaderIdExt, HeaderProvider, StateProviderFactory,
|
||||
noop::NoopProvider, BalProvider, BlockReader, BlockReaderIdExt, HeaderProvider,
|
||||
StateProviderFactory,
|
||||
};
|
||||
use reth_tasks::Runtime;
|
||||
use reth_tokio_util::EventStream;
|
||||
@@ -247,6 +248,7 @@ where
|
||||
Receipt = reth_ethereum_primitives::Receipt,
|
||||
Header = alloy_consensus::Header,
|
||||
> + HeaderProvider
|
||||
+ BalProvider
|
||||
+ Clone
|
||||
+ Unpin
|
||||
+ 'static,
|
||||
@@ -319,6 +321,7 @@ where
|
||||
Receipt = reth_ethereum_primitives::Receipt,
|
||||
Header = alloy_consensus::Header,
|
||||
> + HeaderProvider
|
||||
+ BalProvider
|
||||
+ Unpin
|
||||
+ 'static,
|
||||
Pool: TransactionPool<
|
||||
@@ -462,7 +465,10 @@ where
|
||||
}
|
||||
|
||||
/// Set a new request handler that's connected to the peer's network
|
||||
pub fn install_request_handler(&mut self) {
|
||||
pub fn install_request_handler(&mut self)
|
||||
where
|
||||
C: BalProvider,
|
||||
{
|
||||
let (tx, rx) = channel(ETH_REQUEST_CHANNEL_CAPACITY);
|
||||
self.network.set_eth_request_handler(tx);
|
||||
let peers = self.network.peers_handle();
|
||||
@@ -573,6 +579,7 @@ where
|
||||
Receipt = reth_ethereum_primitives::Receipt,
|
||||
Header = alloy_consensus::Header,
|
||||
> + HeaderProvider
|
||||
+ BalProvider
|
||||
+ Unpin
|
||||
+ 'static,
|
||||
Pool: TransactionPool<
|
||||
|
||||
@@ -2,23 +2,29 @@
|
||||
//! Tests for eth related requests
|
||||
|
||||
use alloy_consensus::Header;
|
||||
use alloy_primitives::{Bytes, B256};
|
||||
use rand::Rng;
|
||||
use reth_eth_wire::{EthVersion, HeadersDirection};
|
||||
use reth_eth_wire::{BlockAccessLists, EthVersion, GetBlockAccessLists, HeadersDirection};
|
||||
use reth_ethereum_primitives::Block;
|
||||
use reth_network::{
|
||||
test_utils::{NetworkEventStream, PeerConfig, Testnet},
|
||||
eth_requests::SOFT_RESPONSE_LIMIT,
|
||||
test_utils::{NetworkEventStream, PeerConfig, Testnet, TestnetHandle},
|
||||
BlockDownloaderProvider, NetworkEventListenerProvider,
|
||||
};
|
||||
use reth_network_api::{NetworkInfo, Peers};
|
||||
use reth_network_p2p::{
|
||||
bodies::client::BodiesClient,
|
||||
error::RequestError,
|
||||
headers::client::{HeadersClient, HeadersRequest},
|
||||
BalRequirement, BlockAccessListsClient,
|
||||
};
|
||||
use reth_provider::test_utils::MockEthProvider;
|
||||
use reth_provider::{test_utils::MockEthProvider, BalStoreHandle, InMemoryBalStore};
|
||||
use reth_transaction_pool::test_utils::{TestPool, TransactionGenerator};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
type BalTestnetHandle = TestnetHandle<Arc<MockEthProvider>, TestPool>;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_get_body() {
|
||||
reth_tracing::init_test_tracing();
|
||||
@@ -526,3 +532,178 @@ async fn test_eth69_get_receipts() {
|
||||
assert_eq!(receipts_response.0[0][1].cumulative_gas_used, 42000);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth71_get_block_access_lists() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, bal_store) = spawn_eth71_bal_testnet().await;
|
||||
|
||||
let hash0 = B256::random();
|
||||
let hash1 = B256::random();
|
||||
let hash2 = B256::random();
|
||||
let bal0 = Bytes::from_static(&[0xc1, 0x01]);
|
||||
let bal2 = Bytes::from_static(&[0xc1, 0x02]);
|
||||
|
||||
bal_store.insert(hash0, 1, bal0.clone()).unwrap();
|
||||
bal_store.insert(hash2, 3, bal2.clone()).unwrap();
|
||||
|
||||
let response = request_block_access_lists(&net, vec![hash0, hash1, hash2]).await;
|
||||
assert_eq!(
|
||||
response,
|
||||
BlockAccessLists(vec![bal0, Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE]), bal2,])
|
||||
);
|
||||
}
|
||||
|
||||
// Ensures BAL responses stop at the soft response limit while keeping the item that crosses it.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth71_get_block_access_lists_respects_response_soft_limit() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, bal_store) = spawn_eth71_bal_testnet().await;
|
||||
|
||||
let hash0 = B256::random();
|
||||
let hash1 = B256::random();
|
||||
let hash2 = B256::random();
|
||||
let bal0 = raw_bal_with_len(2);
|
||||
let bal1 = raw_bal_with_len(SOFT_RESPONSE_LIMIT);
|
||||
let bal2 = raw_bal_with_len(2);
|
||||
assert!(bal0.len() + bal1.len() > SOFT_RESPONSE_LIMIT);
|
||||
|
||||
bal_store.insert(hash0, 1, bal0.clone()).unwrap();
|
||||
bal_store.insert(hash1, 2, bal1.clone()).unwrap();
|
||||
bal_store.insert(hash2, 3, bal2).unwrap();
|
||||
|
||||
let response = request_block_access_lists(&net, vec![hash0, hash1, hash2]).await;
|
||||
|
||||
assert_eq!(response, BlockAccessLists(vec![bal0, bal1]));
|
||||
}
|
||||
|
||||
// Ensures a single BAL larger than the soft limit is still returned.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth71_get_block_access_lists_returns_single_oversized_bal() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, bal_store) = spawn_eth71_bal_testnet().await;
|
||||
|
||||
let hash0 = B256::random();
|
||||
let hash1 = B256::random();
|
||||
let bal0 = raw_bal_with_len(SOFT_RESPONSE_LIMIT + 1);
|
||||
let bal1 = raw_bal_with_len(2);
|
||||
|
||||
bal_store.insert(hash0, 1, bal0.clone()).unwrap();
|
||||
bal_store.insert(hash1, 2, bal1).unwrap();
|
||||
|
||||
let response = request_block_access_lists(&net, vec![hash0, hash1]).await;
|
||||
|
||||
assert_eq!(response, BlockAccessLists(vec![bal0]));
|
||||
}
|
||||
|
||||
// Ensures an empty BAL request roundtrips to an empty response.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth71_get_block_access_lists_empty_request() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, _) = spawn_eth71_bal_testnet().await;
|
||||
|
||||
let response = request_block_access_lists(&net, Vec::new()).await;
|
||||
|
||||
assert_eq!(response, BlockAccessLists(Vec::new()));
|
||||
}
|
||||
|
||||
// Ensures the fetch client can request BALs through an eth/71 peer.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth71_fetch_client_get_block_access_lists() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, bal_store) = spawn_eth71_bal_testnet().await;
|
||||
|
||||
let hash0 = B256::random();
|
||||
let hash1 = B256::random();
|
||||
let bal0 = Bytes::from_static(&[0xc1, 0x01]);
|
||||
|
||||
bal_store.insert(hash0, 1, bal0.clone()).unwrap();
|
||||
|
||||
let fetch = net.peers()[0].network().fetch_client().await.unwrap();
|
||||
let response = fetch.get_block_access_lists(vec![hash0, hash1]).await.unwrap().into_data();
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
BlockAccessLists(vec![bal0, Bytes::from_static(&[alloy_rlp::EMPTY_LIST_CODE])])
|
||||
);
|
||||
}
|
||||
|
||||
// Ensures fetch client BAL requests are rejected when no eth/71 peer is available.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_eth70_fetch_client_rejects_optional_block_access_lists_request() {
|
||||
reth_tracing::init_test_tracing();
|
||||
let (net, _) = spawn_bal_testnet([EthVersion::Eth70, EthVersion::Eth70]).await;
|
||||
|
||||
let fetch = net.peers()[0].network().fetch_client().await.unwrap();
|
||||
let err = fetch
|
||||
.get_block_access_lists_with_requirement(vec![B256::random()], BalRequirement::Optional)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err, RequestError::UnsupportedCapability);
|
||||
}
|
||||
|
||||
async fn spawn_eth71_bal_testnet() -> (BalTestnetHandle, BalStoreHandle) {
|
||||
spawn_bal_testnet([EthVersion::Eth71, EthVersion::Eth71]).await
|
||||
}
|
||||
|
||||
// Spawns a BAL testnet with one peer per requested eth protocol version.
|
||||
async fn spawn_bal_testnet(
|
||||
versions: impl IntoIterator<Item = EthVersion>,
|
||||
) -> (BalTestnetHandle, BalStoreHandle) {
|
||||
let mut mock_provider = MockEthProvider::default();
|
||||
let bal_store = BalStoreHandle::new(InMemoryBalStore::default());
|
||||
mock_provider.bal_store = bal_store.clone();
|
||||
let mock_provider = Arc::new(mock_provider);
|
||||
|
||||
let mut net: Testnet<Arc<MockEthProvider>, TestPool> = Testnet::default();
|
||||
|
||||
for version in versions {
|
||||
let peer = PeerConfig::with_protocols(mock_provider.clone(), Some(version.into()));
|
||||
net.add_peer_with_config(peer).await.unwrap();
|
||||
}
|
||||
|
||||
net.for_each_mut(|peer| peer.install_request_handler());
|
||||
|
||||
let net = net.spawn();
|
||||
net.connect_peers().await;
|
||||
|
||||
(net, bal_store)
|
||||
}
|
||||
|
||||
// Sends a GetBlockAccessLists request from peer 0 to peer 1.
|
||||
async fn request_block_access_lists(net: &BalTestnetHandle, hashes: Vec<B256>) -> BlockAccessLists {
|
||||
let requester = &net.peers()[0];
|
||||
let responder = &net.peers()[1];
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
requester.network().send_request(
|
||||
*responder.peer_id(),
|
||||
reth_network::PeerRequest::GetBlockAccessLists {
|
||||
request: GetBlockAccessLists(hashes),
|
||||
response: tx,
|
||||
},
|
||||
);
|
||||
|
||||
rx.await.unwrap().unwrap()
|
||||
}
|
||||
|
||||
// Builds a complete raw RLP list item with the requested encoded byte length.
|
||||
fn raw_bal_with_len(len: usize) -> Bytes {
|
||||
assert!(len > 0);
|
||||
|
||||
let mut payload_length = len - 1;
|
||||
loop {
|
||||
let header_length = alloy_rlp::Header { list: true, payload_length }.length();
|
||||
let next_payload_length = len.checked_sub(header_length).unwrap();
|
||||
if next_payload_length == payload_length {
|
||||
break
|
||||
}
|
||||
payload_length = next_payload_length;
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(len);
|
||||
alloy_rlp::Header { list: true, payload_length }.encode(&mut out);
|
||||
out.resize(len, alloy_rlp::EMPTY_LIST_CODE);
|
||||
Bytes::from(out)
|
||||
}
|
||||
|
||||
@@ -169,7 +169,10 @@ pub fn get_filter_block_range(
|
||||
|
||||
// we cannot query blocks that don't exist yet
|
||||
if to_block_number > info.best_number {
|
||||
return Err(FilterBlockRangeError::BlockRangeExceedsHead);
|
||||
return Err(FilterBlockRangeError::BlockRangeExceedsHead {
|
||||
requested: to_block_number,
|
||||
head: info.best_number,
|
||||
});
|
||||
}
|
||||
|
||||
Ok((from_block_number, to_block_number))
|
||||
@@ -184,8 +187,13 @@ pub enum FilterBlockRangeError {
|
||||
#[error("invalid block range params")]
|
||||
InvalidBlockRange,
|
||||
/// Block range extends beyond current head
|
||||
#[error("block range extends beyond current head block")]
|
||||
BlockRangeExceedsHead,
|
||||
#[error("block range extends beyond current head block: requested {requested}, head {head}")]
|
||||
BlockRangeExceedsHead {
|
||||
/// The requested `toBlock` number
|
||||
requested: u64,
|
||||
/// The current head block number
|
||||
head: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -227,7 +235,10 @@ mod tests {
|
||||
let to = 15000002u64;
|
||||
let info = ChainInfo { best_number: 15000000, ..Default::default() };
|
||||
let err = get_filter_block_range(Some(from), Some(to), info.best_number, info).unwrap_err();
|
||||
assert_eq!(err, FilterBlockRangeError::BlockRangeExceedsHead);
|
||||
assert_eq!(
|
||||
err,
|
||||
FilterBlockRangeError::BlockRangeExceedsHead { requested: to, head: info.best_number }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -263,7 +274,10 @@ mod tests {
|
||||
let to = 200;
|
||||
let info = ChainInfo { best_number: 150, ..Default::default() };
|
||||
let err = get_filter_block_range(Some(from), Some(to), 0, info).unwrap_err();
|
||||
assert_eq!(err, FilterBlockRangeError::BlockRangeExceedsHead);
|
||||
assert_eq!(
|
||||
err,
|
||||
FilterBlockRangeError::BlockRangeExceedsHead { requested: to, head: info.best_number }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -564,7 +564,10 @@ where
|
||||
if let Some(t) = to &&
|
||||
t > info.best_number
|
||||
{
|
||||
return Err(EthFilterError::BlockRangeExceedsHead);
|
||||
return Err(EthFilterError::BlockRangeExceedsHead {
|
||||
requested: t,
|
||||
head: info.best_number,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(f) = from &&
|
||||
@@ -942,8 +945,13 @@ pub enum EthFilterError {
|
||||
#[error("invalid block range params")]
|
||||
InvalidBlockRangeParams,
|
||||
/// Block range extends beyond current head.
|
||||
#[error("block range extends beyond current head block")]
|
||||
BlockRangeExceedsHead,
|
||||
#[error("block range extends beyond current head block: requested {requested}, head {head}")]
|
||||
BlockRangeExceedsHead {
|
||||
/// The requested `toBlock` number
|
||||
requested: u64,
|
||||
/// The current head block number
|
||||
head: u64,
|
||||
},
|
||||
/// Query scope is too broad.
|
||||
#[error("query exceeds max block range {0}")]
|
||||
QueryExceedsMaxBlocks(u64),
|
||||
@@ -979,7 +987,7 @@ impl From<EthFilterError> for jsonrpsee::types::error::ErrorObject<'static> {
|
||||
err @ (EthFilterError::InvalidBlockRangeParams |
|
||||
EthFilterError::QueryExceedsMaxBlocks(_) |
|
||||
EthFilterError::QueryExceedsMaxResults { .. } |
|
||||
EthFilterError::BlockRangeExceedsHead) => {
|
||||
EthFilterError::BlockRangeExceedsHead { .. }) => {
|
||||
rpc_error_with_code(jsonrpsee::types::error::INVALID_PARAMS_CODE, err.to_string())
|
||||
}
|
||||
}
|
||||
@@ -996,7 +1004,9 @@ impl From<logs_utils::FilterBlockRangeError> for EthFilterError {
|
||||
fn from(err: logs_utils::FilterBlockRangeError) -> Self {
|
||||
match err {
|
||||
logs_utils::FilterBlockRangeError::InvalidBlockRange => Self::InvalidBlockRangeParams,
|
||||
logs_utils::FilterBlockRangeError::BlockRangeExceedsHead => Self::BlockRangeExceedsHead,
|
||||
logs_utils::FilterBlockRangeError::BlockRangeExceedsHead { requested, head } => {
|
||||
Self::BlockRangeExceedsHead { requested, head }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
109
crates/storage/provider/src/bal.rs
Normal file
109
crates/storage/provider/src/bal.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use alloy_primitives::{BlockHash, BlockNumber, Bytes};
|
||||
use parking_lot::RwLock;
|
||||
use reth_storage_api::{BalStore, GetBlockAccessListLimit};
|
||||
use reth_storage_errors::provider::ProviderResult;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
/// Basic in-memory BAL store keyed by block hash.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct InMemoryBalStore {
|
||||
entries: Arc<RwLock<HashMap<BlockHash, Bytes>>>,
|
||||
}
|
||||
|
||||
impl BalStore for InMemoryBalStore {
|
||||
fn insert(
|
||||
&self,
|
||||
block_hash: BlockHash,
|
||||
_block_number: BlockNumber,
|
||||
bal: Bytes,
|
||||
) -> ProviderResult<()> {
|
||||
self.entries.write().insert(block_hash, bal);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> ProviderResult<Vec<Option<Bytes>>> {
|
||||
let entries = self.entries.read();
|
||||
let mut result = Vec::with_capacity(block_hashes.len());
|
||||
|
||||
for hash in block_hashes {
|
||||
result.push(entries.get(hash).cloned());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn append_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
out: &mut Vec<Bytes>,
|
||||
) -> ProviderResult<()> {
|
||||
let entries = self.entries.read();
|
||||
let mut size = 0;
|
||||
|
||||
for hash in block_hashes {
|
||||
let bal = entries.get(hash).cloned().unwrap_or_else(|| Bytes::from_static(&[0xc0]));
|
||||
size += bal.len();
|
||||
out.push(bal);
|
||||
|
||||
if limit.exceeds(size) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_by_range(&self, _start: BlockNumber, _count: u64) -> ProviderResult<Vec<Bytes>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloy_primitives::B256;
|
||||
|
||||
#[test]
|
||||
fn insert_and_lookup_by_hash() {
|
||||
let store = InMemoryBalStore::default();
|
||||
let hash = B256::random();
|
||||
let missing = B256::random();
|
||||
let bal = Bytes::from_static(b"bal");
|
||||
|
||||
store.insert(hash, 1, bal.clone()).unwrap();
|
||||
|
||||
assert_eq!(store.get_by_hashes(&[hash, missing]).unwrap(), vec![Some(bal), None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_lookup_is_empty() {
|
||||
let store = InMemoryBalStore::default();
|
||||
|
||||
assert!(store.get_by_range(1, 10).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limited_lookup_returns_prefix() {
|
||||
let store = InMemoryBalStore::default();
|
||||
let hash0 = B256::random();
|
||||
let hash1 = B256::random();
|
||||
let hash2 = B256::random();
|
||||
let bal0 = Bytes::from_static(&[0xc1, 0x01]);
|
||||
let bal1 = Bytes::from_static(&[0xc1, 0x02]);
|
||||
let bal2 = Bytes::from_static(&[0xc1, 0x03]);
|
||||
|
||||
store.insert(hash0, 1, bal0.clone()).unwrap();
|
||||
store.insert(hash1, 2, bal1.clone()).unwrap();
|
||||
store.insert(hash2, 3, bal2).unwrap();
|
||||
|
||||
let limited = store
|
||||
.get_by_hashes_with_limit(
|
||||
&[hash0, hash1, hash2],
|
||||
GetBlockAccessListLimit::ResponseSizeSoftLimit(2),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(limited, vec![bal0, bal1]);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,9 @@ pub mod test_utils;
|
||||
pub mod either_writer;
|
||||
pub use either_writer::*;
|
||||
|
||||
mod bal;
|
||||
pub use bal::InMemoryBalStore;
|
||||
|
||||
pub use reth_chain_state::{
|
||||
CanonStateNotification, CanonStateNotificationSender, CanonStateNotificationStream,
|
||||
CanonStateNotifications, CanonStateSubscriptions,
|
||||
@@ -48,8 +51,9 @@ pub use revm_database::states::OriginalValuesKnown;
|
||||
// reexport traits to avoid breaking changes
|
||||
pub use reth_static_file_types as static_file;
|
||||
pub use reth_storage_api::{
|
||||
BalProvider, BalStore, BalStoreHandle, HistoryWriter, MetadataProvider, MetadataWriter,
|
||||
NoopBalStore, StateWriteConfig, StatsReader, StorageSettings, StorageSettingsCache,
|
||||
BalProvider, BalStore, BalStoreHandle, GetBlockAccessListLimit, HistoryWriter,
|
||||
MetadataProvider, MetadataWriter, NoopBalStore, StateWriteConfig, StatsReader, StorageSettings,
|
||||
StorageSettingsCache,
|
||||
};
|
||||
/// Re-export provider error.
|
||||
pub use reth_storage_errors::provider::{ProviderError, ProviderResult};
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::{
|
||||
AccountReader, BalProvider, BalStoreHandle, BlockHashReader, BlockIdReader, BlockNumReader,
|
||||
BlockReader, BlockReaderIdExt, BlockSource, CanonChainTracker, CanonStateNotifications,
|
||||
CanonStateSubscriptions, ChainSpecProvider, ChainStateBlockReader, ChangeSetReader,
|
||||
DatabaseProviderFactory, HashedPostStateProvider, HeaderProvider, ProviderError,
|
||||
ProviderFactory, PruneCheckpointReader, ReceiptProvider, ReceiptProviderIdExt,
|
||||
DatabaseProviderFactory, HashedPostStateProvider, HeaderProvider, InMemoryBalStore,
|
||||
ProviderError, ProviderFactory, PruneCheckpointReader, ReceiptProvider, ReceiptProviderIdExt,
|
||||
RocksDBProviderFactory, StageCheckpointReader, StateProviderBox, StateProviderFactory,
|
||||
StateReader, StaticFileProviderFactory, TransactionVariant, TransactionsProvider,
|
||||
};
|
||||
@@ -111,7 +111,7 @@ impl<N: ProviderNodeTypes> BlockchainProvider<N> {
|
||||
finalized_header,
|
||||
safe_header,
|
||||
),
|
||||
bal_store: BalStoreHandle::default(),
|
||||
bal_store: BalStoreHandle::new(InMemoryBalStore::default()),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ use crate::{
|
||||
},
|
||||
to_range,
|
||||
traits::{BlockSource, ReceiptProvider},
|
||||
BlockHashReader, BlockNumReader, BlockReader, ChainSpecProvider, DatabaseProviderFactory,
|
||||
EitherWriterDestination, HashedPostStateProvider, HeaderProvider, HeaderSyncGapProvider,
|
||||
MetadataProvider, ProviderError, PruneCheckpointReader, RocksDBProviderFactory,
|
||||
StageCheckpointReader, StateProviderBox, StaticFileProviderFactory, StaticFileWriter,
|
||||
TransactionVariant, TransactionsProvider,
|
||||
BalProvider, BalStoreHandle, BlockHashReader, BlockNumReader, BlockReader, ChainSpecProvider,
|
||||
DatabaseProviderFactory, EitherWriterDestination, HashedPostStateProvider, HeaderProvider,
|
||||
HeaderSyncGapProvider, MetadataProvider, ProviderError, PruneCheckpointReader,
|
||||
RocksDBProviderFactory, StageCheckpointReader, StateProviderBox, StaticFileProviderFactory,
|
||||
StaticFileWriter, TransactionVariant, TransactionsProvider,
|
||||
};
|
||||
use alloy_consensus::transaction::TransactionMeta;
|
||||
use alloy_eips::BlockHashOrNumber;
|
||||
@@ -90,6 +90,8 @@ pub struct ProviderFactory<N: NodeTypesWithDB> {
|
||||
rocksdb_provider: RocksDBProvider,
|
||||
/// Changeset cache for trie unwinding
|
||||
changeset_cache: ChangesetCache,
|
||||
/// Store for block access lists.
|
||||
bal_store: BalStoreHandle,
|
||||
/// Task runtime for spawning parallel I/O work.
|
||||
runtime: reth_tasks::Runtime,
|
||||
/// Minimum distance from tip required before pruning can occur.
|
||||
@@ -152,6 +154,7 @@ impl<N: ProviderNodeTypes> ProviderFactory<N> {
|
||||
storage_settings: Arc::new(RwLock::new(storage_settings)),
|
||||
rocksdb_provider,
|
||||
changeset_cache: ChangesetCache::new(),
|
||||
bal_store: BalStoreHandle::default(),
|
||||
runtime,
|
||||
minimum_pruning_distance: MINIMUM_UNWIND_SAFE_DISTANCE,
|
||||
read_only_sync: None,
|
||||
@@ -583,6 +586,12 @@ impl<N: NodeTypesWithDB> NodePrimitivesProvider for ProviderFactory<N> {
|
||||
type Primitives = N::Primitives;
|
||||
}
|
||||
|
||||
impl<N: NodeTypesWithDB> BalProvider for ProviderFactory<N> {
|
||||
fn bal_store(&self) -> &BalStoreHandle {
|
||||
&self.bal_store
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: ProviderNodeTypes> DatabaseProviderFactory for ProviderFactory<N> {
|
||||
type DB = N::DB;
|
||||
type Provider = DatabaseProvider<<N::DB as Database>::TX, N>;
|
||||
@@ -955,6 +964,7 @@ where
|
||||
storage_settings,
|
||||
rocksdb_provider,
|
||||
changeset_cache,
|
||||
bal_store,
|
||||
runtime,
|
||||
minimum_pruning_distance,
|
||||
read_only_sync,
|
||||
@@ -968,6 +978,7 @@ where
|
||||
.field("storage_settings", &*storage_settings.read())
|
||||
.field("rocksdb_provider", &rocksdb_provider)
|
||||
.field("changeset_cache", &changeset_cache)
|
||||
.field("bal_store", &bal_store)
|
||||
.field("runtime", &runtime)
|
||||
.field("minimum_pruning_distance", &minimum_pruning_distance)
|
||||
.field(
|
||||
@@ -989,6 +1000,7 @@ impl<N: NodeTypesWithDB> Clone for ProviderFactory<N> {
|
||||
storage_settings: self.storage_settings.clone(),
|
||||
rocksdb_provider: self.rocksdb_provider.clone(),
|
||||
changeset_cache: self.changeset_cache.clone(),
|
||||
bal_store: self.bal_store.clone(),
|
||||
runtime: self.runtime.clone(),
|
||||
minimum_pruning_distance: self.minimum_pruning_distance,
|
||||
read_only_sync: self.read_only_sync.clone(),
|
||||
|
||||
@@ -22,12 +22,70 @@ pub trait BalStore: Send + Sync + 'static {
|
||||
/// The returned vector must align with `block_hashes`.
|
||||
fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> ProviderResult<Vec<Option<Bytes>>>;
|
||||
|
||||
/// Fetch BAL response entries for the given block hashes, stopping after the soft limit is
|
||||
/// exceeded.
|
||||
///
|
||||
/// Entries are returned in request order. Unavailable BALs are represented as an RLP-encoded
|
||||
/// empty list (`0xc0`). The limit is soft: the entry that exceeds the limit is included.
|
||||
fn get_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
) -> ProviderResult<Vec<Bytes>> {
|
||||
let mut out = Vec::new();
|
||||
self.append_by_hashes_with_limit(block_hashes, limit, &mut out)?;
|
||||
out.shrink_to_fit();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Extends the given vector with BAL response entries for the given hashes.
|
||||
///
|
||||
/// This adheres to the expected behavior of [`Self::get_by_hashes_with_limit`].
|
||||
fn append_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
out: &mut Vec<Bytes>,
|
||||
) -> ProviderResult<()> {
|
||||
let mut size = 0;
|
||||
for bal in self.get_by_hashes(block_hashes)? {
|
||||
let bal = bal.unwrap_or_else(|| Bytes::from_static(&[0xc0]));
|
||||
size += bal.len();
|
||||
out.push(bal);
|
||||
|
||||
if limit.exceeds(size) {
|
||||
break
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch BALs for the requested range.
|
||||
///
|
||||
/// Implementations may stop at the first gap and return the contiguous prefix.
|
||||
fn get_by_range(&self, start: BlockNumber, count: u64) -> ProviderResult<Vec<Bytes>>;
|
||||
}
|
||||
|
||||
/// The limit to enforce for [`BalStore::get_by_hashes_with_limit`].
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum GetBlockAccessListLimit {
|
||||
/// No limit, return all BALs.
|
||||
None,
|
||||
/// Enforce a size limit on the returned BALs, for example 2MB.
|
||||
ResponseSizeSoftLimit(usize),
|
||||
}
|
||||
|
||||
impl GetBlockAccessListLimit {
|
||||
/// Returns true if the given size exceeds the limit.
|
||||
#[inline]
|
||||
pub const fn exceeds(&self, size: usize) -> bool {
|
||||
match self {
|
||||
Self::None => false,
|
||||
Self::ResponseSizeSoftLimit(limit) => size > *limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone-friendly façade around a BAL store implementation.
|
||||
#[derive(Clone)]
|
||||
pub struct BalStoreHandle {
|
||||
@@ -62,6 +120,28 @@ impl BalStoreHandle {
|
||||
self.inner.get_by_hashes(block_hashes)
|
||||
}
|
||||
|
||||
/// Fetch BAL response entries for the given block hashes, stopping after the soft limit is
|
||||
/// exceeded.
|
||||
#[inline]
|
||||
pub fn get_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
) -> ProviderResult<Vec<Bytes>> {
|
||||
self.inner.get_by_hashes_with_limit(block_hashes, limit)
|
||||
}
|
||||
|
||||
/// Extends the given vector with BAL response entries for the given hashes.
|
||||
#[inline]
|
||||
pub fn append_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
out: &mut Vec<Bytes>,
|
||||
) -> ProviderResult<()> {
|
||||
self.inner.append_by_hashes_with_limit(block_hashes, limit, out)
|
||||
}
|
||||
|
||||
/// Fetch BALs for the requested range.
|
||||
#[inline]
|
||||
pub fn get_by_range(&self, start: BlockNumber, count: u64) -> ProviderResult<Vec<Bytes>> {
|
||||
@@ -106,6 +186,25 @@ impl BalStore for NoopBalStore {
|
||||
Ok(block_hashes.iter().map(|_| None).collect())
|
||||
}
|
||||
|
||||
fn append_by_hashes_with_limit(
|
||||
&self,
|
||||
block_hashes: &[BlockHash],
|
||||
limit: GetBlockAccessListLimit,
|
||||
out: &mut Vec<Bytes>,
|
||||
) -> ProviderResult<()> {
|
||||
let mut size = 0;
|
||||
for _ in block_hashes {
|
||||
let bal = Bytes::from_static(&[0xc0]);
|
||||
size += bal.len();
|
||||
out.push(bal);
|
||||
|
||||
if limit.exceeds(size) {
|
||||
break
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_by_range(&self, _start: BlockNumber, _count: u64) -> ProviderResult<Vec<Bytes>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
@@ -127,4 +226,27 @@ mod tests {
|
||||
assert_eq!(by_hash, vec![None, None]);
|
||||
assert!(by_range.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noop_store_limited_lookup_returns_prefix() {
|
||||
let store = BalStoreHandle::default();
|
||||
let hashes = [B256::random(), B256::random(), B256::random()];
|
||||
|
||||
let limited = store
|
||||
.get_by_hashes_with_limit(&hashes, GetBlockAccessListLimit::ResponseSizeSoftLimit(1))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(limited, vec![Bytes::from_static(&[0xc0]), Bytes::from_static(&[0xc0])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_access_list_limit() {
|
||||
let limit_none = GetBlockAccessListLimit::None;
|
||||
assert!(!limit_none.exceeds(usize::MAX));
|
||||
|
||||
let size_limit_2mb = GetBlockAccessListLimit::ResponseSizeSoftLimit(2 * 1024 * 1024);
|
||||
assert!(!size_limit_2mb.exceeds(1024 * 1024));
|
||||
assert!(!size_limit_2mb.exceeds(2 * 1024 * 1024));
|
||||
assert!(size_limit_2mb.exceeds(3 * 1024 * 1024));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,7 +547,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
ensure_intrinsic_gas(transaction, &self.fork_tracker)?;
|
||||
ensure_intrinsic_gas(transaction, &self.fork_tracker, block_gas_limit)?;
|
||||
|
||||
// light blob tx pre-checks
|
||||
if transaction.is_eip4844() {
|
||||
@@ -1404,6 +1404,7 @@ impl ForkTracker {
|
||||
pub fn ensure_intrinsic_gas<T: EthPoolTransaction>(
|
||||
transaction: &T,
|
||||
fork_tracker: &ForkTracker,
|
||||
block_gas_limit: u64,
|
||||
) -> Result<(), InvalidPoolTransactionError> {
|
||||
use revm_primitives::hardfork::SpecId;
|
||||
let spec_id = if fork_tracker.is_prague_activated() {
|
||||
@@ -1424,6 +1425,7 @@ pub fn ensure_intrinsic_gas<T: EthPoolTransaction>(
|
||||
.map(|l| l.iter().map(|i| i.storage_keys.len()).sum::<usize>())
|
||||
.unwrap_or_default() as u64,
|
||||
transaction.authorization_list().map(|l| l.len()).unwrap_or_default() as u64,
|
||||
revm_primitives::eip8037::cost_per_state_byte(block_gas_limit),
|
||||
);
|
||||
|
||||
let gas_limit = transaction.gas_limit();
|
||||
@@ -1478,11 +1480,11 @@ mod tests {
|
||||
tx_gas_limit_cap: AtomicU64::new(0),
|
||||
};
|
||||
|
||||
let res = ensure_intrinsic_gas(&transaction, &fork_tracker);
|
||||
let res = ensure_intrinsic_gas(&transaction, &fork_tracker, 30_000_000);
|
||||
assert!(res.is_ok());
|
||||
|
||||
fork_tracker.shanghai = true.into();
|
||||
let res = ensure_intrinsic_gas(&transaction, &fork_tracker);
|
||||
let res = ensure_intrinsic_gas(&transaction, &fork_tracker, 30_000_000);
|
||||
assert!(res.is_ok());
|
||||
|
||||
let provider = MockEthProvider::default().with_genesis_block();
|
||||
|
||||
Reference in New Issue
Block a user