Compare commits

...

11 Commits

Author SHA1 Message Date
rakita
7b2c458302 feat(pool): derive EIP-8037 CPSB from block gas limit in ensure_intrinsic_gas
Thread the current block gas limit through ensure_intrinsic_gas and
compute cost_per_state_byte via revm_primitives::eip8037 instead of
hard-coding 0. Pool callers already have block_gas_limit on hand.
2026-04-27 08:41:33 +02:00
rakita
0722202930 chore: integrate revm devnet4 + paired forks (rev fe2549d8)
- revm: fe2549d85fb9e201e7b629f8b47bcca46d49aa1d
- revm-inspectors: a2c7a41977b468d016a339f560acb76e002766f3
- alloy-evm: da7633f6bc9554f5a6e60773ef21b8e9d6e0cca6

Adapt to revm Account: original_info is now a private
Option<Box<AccountInfo>>; build accounts via Account::default()
in the engine tree payload processor test fixture.
2026-04-27 00:44:33 +02:00
rakita
8ec6e614f9 chore: integrate revm devnet4 + paired forks (rev 7a2de5a4)
Patch revm to devnet4 (EIP-8037 dynamic CPSB, EIP-7981 access list cost
increase, EIP-7976 calldata floor cost bump, EIP-8037 reservoir refill)
along with the corresponding devnet4 commits of revm-inspectors,
alloy-evm, and reth-core.

Pin revm/revm-inspectors workspace deps to exact `=X.Y.Z` versions so
`[patch.crates-io]` reliably wins over the slightly higher published
versions (e.g. 13.0.1) currently on crates.io.

Pass cpsb=0 to `calculate_initial_tx_gas` from the txpool validator —
the new EIP-8037 storage-cap argument is irrelevant pre-Amsterdam, and
the pool fork tracker tops out at Prague.
2026-04-26 22:45:06 +02:00
Karl Yu
2c86c0b876 feat(network): add BAL request e2e coverage (#23727) 2026-04-26 18:38:30 +00:00
CPerezz
bd4cd28a8d fix(cli): avoid u64 underflow in setup_without_evm for genesis-block header (#23728) 2026-04-26 14:22:45 +00:00
Karl Yu
6fa48a497a feat(net): enforce BAL response soft limit (#23725) 2026-04-26 05:29:28 +00:00
Arsenii Kulikov
6886cd7742 feat(re-execute): verify reverts against changesets (#23717) 2026-04-25 16:46:35 +00:00
Karl Yu
eeb223f0b8 feat(net): add Basic in-memory BAL store (#23710) 2026-04-25 11:45:29 +00:00
Alexey Shekhirin
f344f5abfb bench: enable keccak-cache-global feature in reth-bb binary (#23723) 2026-04-25 11:19:23 +00:00
JOJO
68845d1114 fix(rpc): include block numbers in BlockRangeExceedsHead error (#23720) 2026-04-25 05:44:50 +00:00
Brian Picciano
ecfb6cc089 fix(ci): clean bench checkouts and lock cargo builds (#23708)
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-04-25 05:39:38 +00:00
22 changed files with 969 additions and 323 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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" }

View File

@@ -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",

View File

@@ -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"
);
}
}

View File

@@ -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(())
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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,
{

View File

@@ -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<

View File

@@ -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)
}

View File

@@ -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]

View File

@@ -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 }
}
}
}
}

View 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]);
}
}

View File

@@ -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};

View File

@@ -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()),
})
}

View File

@@ -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(),

View File

@@ -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));
}
}

View File

@@ -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();