From 6a245ea61dc47cca9ab3d827ffb3c006b8fc14ae Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Tue, 2 Dec 2025 14:21:40 -0600 Subject: [PATCH 1/7] feat: add block metering RPC endpoints Add base_meterBlockByHash and base_meterBlockByNumber RPC methods that re-execute a block and return timing metrics for EVM execution and state root calculation. - Add MeterBlockResponse and MeterBlockTransactions types - Rename meter module to bundle, add new block module for block metering - Include per-transaction timing and gas usage data --- Cargo.lock | 1 + crates/metering/Cargo.toml | 1 + crates/metering/src/block.rs | 107 ++++++++++++ crates/metering/src/{meter.rs => bundle.rs} | 0 crates/metering/src/lib.rs | 8 +- crates/metering/src/rpc.rs | 171 +++++++++++++++++++- crates/metering/src/types.rs | 35 ++++ 7 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 crates/metering/src/block.rs rename crates/metering/src/{meter.rs => bundle.rs} (100%) create mode 100644 crates/metering/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index c21d46d6..41ab737b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1624,6 +1624,7 @@ dependencies = [ "reth-testing-utils", "reth-tracing", "reth-transaction-pool", + "serde", "serde_json", "tips-core", "tokio", diff --git a/crates/metering/Cargo.toml b/crates/metering/Cargo.toml index aeac8afa..52532f5f 100644 --- a/crates/metering/Cargo.toml +++ b/crates/metering/Cargo.toml @@ -40,6 +40,7 @@ jsonrpsee.workspace = true # misc tracing.workspace = true eyre.workspace = true +serde.workspace = true [dev-dependencies] alloy-genesis.workspace = true diff --git a/crates/metering/src/block.rs b/crates/metering/src/block.rs new file mode 100644 index 00000000..24f5efaf --- /dev/null +++ b/crates/metering/src/block.rs @@ -0,0 +1,107 @@ +use std::{sync::Arc, time::Instant}; + +use alloy_consensus::{BlockHeader, transaction::SignerRecoverable}; +use alloy_primitives::B256; +use eyre::{Result as EyreResult, eyre}; +use reth::revm::db::State; +use reth_evm::{ConfigureEvm, execute::BlockBuilder}; +use reth_optimism_chainspec::OpChainSpec; +use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; +use reth_optimism_primitives::OpBlock; +use reth_primitives_traits::{Block as BlockT, SealedHeader}; +use reth_provider::{HashedPostStateProvider, StateRootProvider}; + +use crate::types::{MeterBlockResponse, MeterBlockTransactions}; + +/// Re-executes a block and meters execution time, state root calculation time, and total time. +/// +/// Takes a state provider at the parent block, the chain spec, and the block to meter. +/// +/// Returns `MeterBlockResponse` containing: +/// - Block hash +/// - EVM execution time for all transactions +/// - State root calculation time +/// - Total time +/// - Per-transaction timing information +pub fn meter_block( + state_provider: SP, + chain_spec: Arc, + block: &OpBlock, + parent_header: &SealedHeader, +) -> EyreResult +where + SP: reth_provider::StateProvider + StateRootProvider + HashedPostStateProvider, +{ + let block_hash = block.header().hash_slow(); + let block_number = block.header().number(); + let transactions: Vec<_> = block.body().transactions().cloned().collect(); + let tx_count = transactions.len(); + + // Create state database from parent state + let state_db = reth::revm::database::StateProviderDatabase::new(&state_provider); + let mut db = State::builder().with_database(state_db).with_bundle_update().build(); + + // Set up block attributes from the actual block header + let attributes = OpNextBlockEnvAttributes { + timestamp: block.header().timestamp(), + suggested_fee_recipient: block.header().beneficiary(), + prev_randao: block.header().mix_hash().unwrap_or(B256::random()), + gas_limit: block.header().gas_limit(), + parent_beacon_block_root: block.header().parent_beacon_block_root(), + extra_data: block.header().extra_data().clone(), + }; + + // Execute transactions and measure time + let mut transaction_times = Vec::with_capacity(tx_count); + + let evm_start = Instant::now(); + { + let evm_config = OpEvmConfig::optimism(chain_spec); + let mut builder = evm_config.builder_for_next_block(&mut db, parent_header, attributes)?; + + builder.apply_pre_execution_changes()?; + + for tx in &transactions { + let tx_start = Instant::now(); + let tx_hash = tx.tx_hash(); + + // Recover the signer to create a Recovered transaction for execution + let signer = tx.recover_signer() + .map_err(|e| eyre!("Failed to recover signer for tx {}: {}", tx_hash, e))?; + let recovered_tx = alloy_consensus::transaction::Recovered::new_unchecked(tx.clone(), signer); + + let gas_used = builder + .execute_transaction(recovered_tx) + .map_err(|e| eyre!("Transaction {} execution failed: {}", tx_hash, e))?; + + let execution_time = tx_start.elapsed().as_micros(); + + transaction_times.push(MeterBlockTransactions { + tx_hash, + gas_used, + execution_time_us: execution_time, + }); + } + } + let execution_time = evm_start.elapsed().as_micros(); + + // Calculate state root and measure time + let state_root_start = Instant::now(); + let bundle_state = db.bundle_state.clone(); + let hashed_state = state_provider.hashed_post_state(&bundle_state); + let _state_root = state_provider + .state_root(hashed_state) + .map_err(|e| eyre!("Failed to calculate state root: {}", e))?; + let state_root_time = state_root_start.elapsed().as_micros(); + + let total_time = execution_time + state_root_time; + + Ok(MeterBlockResponse { + block_hash, + block_number, + execution_time_us: execution_time, + state_root_time_us: state_root_time, + total_time_us: total_time, + transactions: transaction_times, + }) +} diff --git a/crates/metering/src/meter.rs b/crates/metering/src/bundle.rs similarity index 100% rename from crates/metering/src/meter.rs rename to crates/metering/src/bundle.rs diff --git a/crates/metering/src/lib.rs b/crates/metering/src/lib.rs index 138a3c7d..67afd9d1 100644 --- a/crates/metering/src/lib.rs +++ b/crates/metering/src/lib.rs @@ -1,8 +1,12 @@ -mod meter; +mod block; +mod bundle; mod rpc; +mod types; #[cfg(test)] mod tests; -pub use meter::meter_bundle; +pub use block::meter_block; +pub use bundle::meter_bundle; pub use rpc::{MeteringApiImpl, MeteringApiServer}; pub use tips_core::types::{Bundle, MeterBundleResponse, TransactionResult}; +pub use types::{MeterBlockResponse, MeterBlockTransactions}; diff --git a/crates/metering/src/rpc.rs b/crates/metering/src/rpc.rs index 19cd2eb4..00d11c4e 100644 --- a/crates/metering/src/rpc.rs +++ b/crates/metering/src/rpc.rs @@ -1,17 +1,21 @@ use alloy_consensus::Header; use alloy_eips::BlockNumberOrTag; -use alloy_primitives::U256; +use alloy_primitives::{B256, U256}; use jsonrpsee::{ core::{RpcResult, async_trait}, proc_macros::rpc, }; use reth::providers::BlockReaderIdExt; use reth_optimism_chainspec::OpChainSpec; -use reth_provider::{ChainSpecProvider, StateProviderFactory}; +use reth_optimism_primitives::OpBlock; +use reth_primitives_traits::Block as BlockT; +use reth_provider::{BlockReader, ChainSpecProvider, StateProviderFactory}; use tips_core::types::{Bundle, MeterBundleResponse, ParsedBundle}; use tracing::{error, info}; -use crate::meter_bundle; +use crate::block::meter_block; +use crate::bundle::meter_bundle; +use crate::types::MeterBlockResponse; /// RPC API for transaction metering #[rpc(server, namespace = "base")] @@ -19,6 +23,32 @@ pub trait MeteringApi { /// Simulates and meters a bundle of transactions #[method(name = "meterBundle")] async fn meter_bundle(&self, bundle: Bundle) -> RpcResult; + + /// Handler for: `base_meterBlockByHash` + /// + /// Re-executes a block and returns timing metrics for EVM execution and state root calculation. + /// + /// This method fetches the block by hash, re-executes all transactions against the parent + /// block's state, and measures: + /// - `executionTimeUs`: Time to execute all transactions in the EVM + /// - `stateRootTimeUs`: Time to compute the state root after execution + /// - `totalTimeUs`: Sum of execution and state root calculation time + /// - `meteredTransactions`: Per-transaction execution times and gas usage + #[method(name = "meterBlockByHash")] + async fn meter_block_by_hash(&self, hash: B256) -> RpcResult; + + /// Handler for: `base_meterBlockByNumber` + /// + /// Re-executes a block and returns timing metrics for EVM execution and state root calculation. + /// + /// This method fetches the block by number, re-executes all transactions against the parent + /// block's state, and measures: + /// - `executionTimeUs`: Time to execute all transactions in the EVM + /// - `stateRootTimeUs`: Time to compute the state root after execution + /// - `totalTimeUs`: Sum of execution and state root calculation time + /// - `meteredTransactions`: Per-transaction execution times and gas usage + #[method(name = "meterBlockByNumber")] + async fn meter_block_by_number(&self, number: BlockNumberOrTag) -> RpcResult; } /// Implementation of the metering RPC API @@ -31,6 +61,7 @@ where Provider: StateProviderFactory + ChainSpecProvider + BlockReaderIdExt
+ + BlockReader + Clone, { /// Creates a new instance of MeteringApi @@ -45,6 +76,7 @@ where Provider: StateProviderFactory + ChainSpecProvider + BlockReaderIdExt
+ + BlockReader + Clone + Send + Sync @@ -139,4 +171,137 @@ where total_execution_time_us: total_execution_time, }) } + + async fn meter_block_by_hash(&self, hash: B256) -> RpcResult { + info!(block_hash = %hash, "Starting block metering by hash"); + + let block = self + .provider + .block_by_hash(hash) + .map_err(|e| { + error!(error = %e, "Failed to get block by hash"); + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + format!("Failed to get block: {}", e), + None::<()>, + ) + })? + .ok_or_else(|| { + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InvalidParams.code(), + format!("Block not found: {}", hash), + None::<()>, + ) + })?; + + let response = self.meter_block_internal(&block)?; + + info!( + block_hash = %hash, + execution_time_us = response.execution_time_us, + state_root_time_us = response.state_root_time_us, + total_time_us = response.total_time_us, + "Block metering completed successfully" + ); + + Ok(response) + } + + async fn meter_block_by_number(&self, number: BlockNumberOrTag) -> RpcResult { + info!(block_number = ?number, "Starting block metering by number"); + + let block = self + .provider + .block_by_number_or_tag(number) + .map_err(|e| { + error!(error = %e, "Failed to get block by number"); + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + format!("Failed to get block: {}", e), + None::<()>, + ) + })? + .ok_or_else(|| { + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InvalidParams.code(), + format!("Block not found: {:?}", number), + None::<()>, + ) + })?; + + let response = self.meter_block_internal(&block)?; + + info!( + block_number = ?number, + block_hash = %response.block_hash, + execution_time_us = response.execution_time_us, + state_root_time_us = response.state_root_time_us, + total_time_us = response.total_time_us, + "Block metering completed successfully" + ); + + Ok(response) + } +} + +impl MeteringApiImpl +where + Provider: StateProviderFactory + + ChainSpecProvider + + BlockReaderIdExt
+ + BlockReader + + Clone + + Send + + Sync + + 'static, +{ + /// Internal helper to meter a block's execution + fn meter_block_internal(&self, block: &OpBlock) -> RpcResult { + // Get parent header + let parent_hash = block.header().parent_hash; + let parent_header = self + .provider + .sealed_header_by_hash(parent_hash) + .map_err(|e| { + error!(error = %e, "Failed to get parent header"); + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + format!("Failed to get parent header: {}", e), + None::<()>, + ) + })? + .ok_or_else(|| { + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + format!("Parent block not found: {}", parent_hash), + None::<()>, + ) + })?; + + // Get state provider at parent block + let state_provider = self.provider.state_by_block_hash(parent_hash).map_err(|e| { + error!(error = %e, "Failed to get state provider for parent block"); + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + format!("Failed to get state provider: {}", e), + None::<()>, + ) + })?; + + // Meter the block + meter_block( + state_provider, + self.provider.chain_spec().clone(), + block, + &parent_header, + ) + .map_err(|e| { + error!(error = %e, "Block metering failed"); + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + format!("Block metering failed: {}", e), + None::<()>, + ) + }) + } } diff --git a/crates/metering/src/types.rs b/crates/metering/src/types.rs new file mode 100644 index 00000000..1dc625a9 --- /dev/null +++ b/crates/metering/src/types.rs @@ -0,0 +1,35 @@ +// TODO: Move these types to tips-core alongside MeterBundleResponse + +use alloy_primitives::B256; +use serde::{Deserialize, Serialize}; + +/// Response for block metering RPC calls. +/// Contains the block hash plus timing information for EVM execution and state root calculation. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MeterBlockResponse { + /// The block hash that was metered + pub block_hash: B256, + /// The block number that was metered + pub block_number: u64, + /// Duration of EVM execution in microseconds + pub execution_time_us: u128, + /// Duration of state root calculation in microseconds + pub state_root_time_us: u128, + /// Total duration (EVM execution + state root calculation) in microseconds + pub total_time_us: u128, + /// Per-transaction metering data + pub transactions: Vec, +} + +/// Metering data for a single transaction +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MeterBlockTransactions { + /// Transaction hash + pub tx_hash: B256, + /// Gas used by this transaction + pub gas_used: u64, + /// Execution time in microseconds + pub execution_time_us: u128, +} From 414502956c6439f087074f60309cf1d8e18fc0de Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Tue, 2 Dec 2025 14:44:36 -0600 Subject: [PATCH 2/7] style: apply rustfmt formatting --- crates/metering/src/block.rs | 6 ++++-- crates/metering/src/lib.rs | 2 +- crates/metering/src/rpc.rs | 37 ++++++++++++++++++------------------ 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/crates/metering/src/block.rs b/crates/metering/src/block.rs index 24f5efaf..cf613ece 100644 --- a/crates/metering/src/block.rs +++ b/crates/metering/src/block.rs @@ -66,9 +66,11 @@ where let tx_hash = tx.tx_hash(); // Recover the signer to create a Recovered transaction for execution - let signer = tx.recover_signer() + let signer = tx + .recover_signer() .map_err(|e| eyre!("Failed to recover signer for tx {}: {}", tx_hash, e))?; - let recovered_tx = alloy_consensus::transaction::Recovered::new_unchecked(tx.clone(), signer); + let recovered_tx = + alloy_consensus::transaction::Recovered::new_unchecked(tx.clone(), signer); let gas_used = builder .execute_transaction(recovered_tx) diff --git a/crates/metering/src/lib.rs b/crates/metering/src/lib.rs index 67afd9d1..ce097de6 100644 --- a/crates/metering/src/lib.rs +++ b/crates/metering/src/lib.rs @@ -1,9 +1,9 @@ mod block; mod bundle; mod rpc; -mod types; #[cfg(test)] mod tests; +mod types; pub use block::meter_block; pub use bundle::meter_bundle; diff --git a/crates/metering/src/rpc.rs b/crates/metering/src/rpc.rs index 00d11c4e..c197a44e 100644 --- a/crates/metering/src/rpc.rs +++ b/crates/metering/src/rpc.rs @@ -13,9 +13,7 @@ use reth_provider::{BlockReader, ChainSpecProvider, StateProviderFactory}; use tips_core::types::{Bundle, MeterBundleResponse, ParsedBundle}; use tracing::{error, info}; -use crate::block::meter_block; -use crate::bundle::meter_bundle; -use crate::types::MeterBlockResponse; +use crate::{block::meter_block, bundle::meter_bundle, types::MeterBlockResponse}; /// RPC API for transaction metering #[rpc(server, namespace = "base")] @@ -48,7 +46,10 @@ pub trait MeteringApi { /// - `totalTimeUs`: Sum of execution and state root calculation time /// - `meteredTransactions`: Per-transaction execution times and gas usage #[method(name = "meterBlockByNumber")] - async fn meter_block_by_number(&self, number: BlockNumberOrTag) -> RpcResult; + async fn meter_block_by_number( + &self, + number: BlockNumberOrTag, + ) -> RpcResult; } /// Implementation of the metering RPC API @@ -207,7 +208,10 @@ where Ok(response) } - async fn meter_block_by_number(&self, number: BlockNumberOrTag) -> RpcResult { + async fn meter_block_by_number( + &self, + number: BlockNumberOrTag, + ) -> RpcResult { info!(block_number = ?number, "Starting block metering by number"); let block = self @@ -289,19 +293,14 @@ where })?; // Meter the block - meter_block( - state_provider, - self.provider.chain_spec().clone(), - block, - &parent_header, - ) - .map_err(|e| { - error!(error = %e, "Block metering failed"); - jsonrpsee::types::ErrorObjectOwned::owned( - jsonrpsee::types::ErrorCode::InternalError.code(), - format!("Block metering failed: {}", e), - None::<()>, - ) - }) + meter_block(state_provider, self.provider.chain_spec().clone(), block, &parent_header) + .map_err(|e| { + error!(error = %e, "Block metering failed"); + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + format!("Block metering failed: {}", e), + None::<()>, + ) + }) } } From 26517a5f2b50f2e969a6c76c5c378c9d82dd51b1 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Thu, 11 Dec 2025 17:17:49 -0500 Subject: [PATCH 3/7] test: add tests for block metering RPC endpoints Add unit tests for meter_block function and RPC tests for meterBlockByHash and meterBlockByNumber endpoints using TestHarness to build real blocks via the engine API. Also rename meter.rs to bundle.rs to match implementation structure. --- crates/metering/src/tests/block.rs | 300 ++++++++++++++++++ .../src/tests/{meter.rs => bundle.rs} | 0 crates/metering/src/tests/mod.rs | 4 +- crates/metering/src/tests/rpc.rs | 139 +++++++- 4 files changed, 439 insertions(+), 4 deletions(-) create mode 100644 crates/metering/src/tests/block.rs rename crates/metering/src/tests/{meter.rs => bundle.rs} (100%) diff --git a/crates/metering/src/tests/block.rs b/crates/metering/src/tests/block.rs new file mode 100644 index 00000000..005490f5 --- /dev/null +++ b/crates/metering/src/tests/block.rs @@ -0,0 +1,300 @@ +use std::sync::Arc; + +use alloy_consensus::{BlockHeader, Header, crypto::secp256k1::public_key_to_address}; +use alloy_genesis::GenesisAccount; +use alloy_primitives::{Address, B256, U256}; +use eyre::Context; +use rand::{SeedableRng, rngs::StdRng}; +use reth::{api::NodeTypesWithDBAdapter, chainspec::EthChainSpec}; +use reth_db::{DatabaseEnv, test_utils::TempDatabase}; +use reth_optimism_chainspec::{BASE_MAINNET, OpChainSpec, OpChainSpecBuilder}; +use reth_optimism_node::OpNode; +use reth_optimism_primitives::{OpBlock, OpBlockBody, OpTransactionSigned}; +use reth_primitives_traits::{Block as BlockT, SealedHeader}; +use reth_provider::{HeaderProvider, StateProviderFactory, providers::BlockchainProvider}; +use reth_testing_utils::generators::generate_keys; +use reth_transaction_pool::test_utils::TransactionBuilder; + +use super::utils::create_provider_factory; +use crate::meter_block; + +type NodeTypes = NodeTypesWithDBAdapter>>; + +#[derive(Eq, PartialEq, Debug, Hash, Clone, Copy)] +enum User { + Alice, + Bob, +} + +#[derive(Debug, Clone)] +struct TestHarness { + provider: BlockchainProvider, + header: SealedHeader, + chain_spec: Arc, + user_to_private_key: std::collections::HashMap, +} + +impl TestHarness { + fn signer(&self, u: User) -> B256 { + self.user_to_private_key[&u] + } +} + +fn create_chain_spec(seed: u64) -> (Arc, std::collections::HashMap) { + let keys = generate_keys(&mut StdRng::seed_from_u64(seed), 2); + + let mut private_keys = std::collections::HashMap::new(); + + let alice_key = keys[0]; + let alice_address = public_key_to_address(alice_key.public_key()); + let alice_secret = B256::from(alice_key.secret_bytes()); + private_keys.insert(User::Alice, alice_secret); + + let bob_key = keys[1]; + let bob_address = public_key_to_address(bob_key.public_key()); + let bob_secret = B256::from(bob_key.secret_bytes()); + private_keys.insert(User::Bob, bob_secret); + + let genesis = BASE_MAINNET + .genesis + .clone() + .extend_accounts(vec![ + (alice_address, GenesisAccount::default().with_balance(U256::from(1_000_000_000_u64))), + (bob_address, GenesisAccount::default().with_balance(U256::from(1_000_000_000_u64))), + ]) + .with_gas_limit(100_000_000); + + let spec = + Arc::new(OpChainSpecBuilder::base_mainnet().genesis(genesis).isthmus_activated().build()); + + (spec, private_keys) +} + +fn setup_harness() -> eyre::Result { + let (chain_spec, user_to_private_key) = create_chain_spec(1337); + let factory = create_provider_factory::(chain_spec.clone()); + + reth_db_common::init::init_genesis(&factory).context("initializing genesis state")?; + + let provider = BlockchainProvider::new(factory.clone()).context("creating provider")?; + let header = provider + .sealed_header(0) + .context("fetching genesis header")? + .expect("genesis header exists"); + + Ok(TestHarness { provider, header, chain_spec, user_to_private_key }) +} + +fn create_block_with_transactions( + harness: &TestHarness, + transactions: Vec, +) -> OpBlock { + let header = Header { + parent_hash: harness.header.hash(), + number: harness.header.number() + 1, + timestamp: harness.header.timestamp() + 2, + gas_limit: 30_000_000, + beneficiary: Address::random(), + base_fee_per_gas: Some(1), + // Required for post-Cancun blocks (EIP-4788) + parent_beacon_block_root: Some(B256::ZERO), + ..Default::default() + }; + + let body = OpBlockBody { transactions, ommers: vec![], withdrawals: None }; + + OpBlock::new(header, body) +} + +#[test] +fn meter_block_empty_transactions() -> eyre::Result<()> { + let harness = setup_harness()?; + + let block = create_block_with_transactions(&harness, vec![]); + + let state_provider = harness + .provider + .state_by_block_hash(harness.header.hash()) + .context("getting state provider")?; + + let response = + meter_block(state_provider, harness.chain_spec.clone(), &block, &harness.header)?; + + assert_eq!(response.block_hash, block.header().hash_slow()); + assert_eq!(response.block_number, block.header().number()); + assert!(response.transactions.is_empty()); + assert!(response.execution_time_us > 0, "execution time should be non-zero due to EVM setup"); + assert!(response.state_root_time_us > 0, "state root time should be non-zero"); + assert_eq!(response.total_time_us, response.execution_time_us + response.state_root_time_us); + + Ok(()) +} + +#[test] +fn meter_block_single_transaction() -> eyre::Result<()> { + let harness = setup_harness()?; + + let to = Address::random(); + let signed_tx = TransactionBuilder::default() + .signer(harness.signer(User::Alice)) + .chain_id(harness.chain_spec.chain_id()) + .nonce(0) + .to(to) + .value(1_000) + .gas_limit(21_000) + .max_fee_per_gas(10) + .max_priority_fee_per_gas(1) + .into_eip1559(); + + let tx = + OpTransactionSigned::Eip1559(signed_tx.as_eip1559().expect("eip1559 transaction").clone()); + let tx_hash = tx.tx_hash(); + + let block = create_block_with_transactions(&harness, vec![tx]); + + let state_provider = harness + .provider + .state_by_block_hash(harness.header.hash()) + .context("getting state provider")?; + + let response = + meter_block(state_provider, harness.chain_spec.clone(), &block, &harness.header)?; + + assert_eq!(response.block_hash, block.header().hash_slow()); + assert_eq!(response.block_number, block.header().number()); + assert_eq!(response.transactions.len(), 1); + + let metered_tx = &response.transactions[0]; + assert_eq!(metered_tx.tx_hash, tx_hash); + assert_eq!(metered_tx.gas_used, 21_000); + assert!(metered_tx.execution_time_us > 0, "transaction execution time should be non-zero"); + + assert!(response.execution_time_us > 0); + assert!(response.state_root_time_us > 0); + assert_eq!(response.total_time_us, response.execution_time_us + response.state_root_time_us); + + Ok(()) +} + +#[test] +fn meter_block_multiple_transactions() -> eyre::Result<()> { + let harness = setup_harness()?; + + let to_1 = Address::random(); + let to_2 = Address::random(); + + // Create first transaction from Alice + let signed_tx_1 = TransactionBuilder::default() + .signer(harness.signer(User::Alice)) + .chain_id(harness.chain_spec.chain_id()) + .nonce(0) + .to(to_1) + .value(1_000) + .gas_limit(21_000) + .max_fee_per_gas(10) + .max_priority_fee_per_gas(1) + .into_eip1559(); + + let tx_1 = OpTransactionSigned::Eip1559( + signed_tx_1.as_eip1559().expect("eip1559 transaction").clone(), + ); + let tx_hash_1 = tx_1.tx_hash(); + + // Create second transaction from Bob + let signed_tx_2 = TransactionBuilder::default() + .signer(harness.signer(User::Bob)) + .chain_id(harness.chain_spec.chain_id()) + .nonce(0) + .to(to_2) + .value(2_000) + .gas_limit(21_000) + .max_fee_per_gas(15) + .max_priority_fee_per_gas(2) + .into_eip1559(); + + let tx_2 = OpTransactionSigned::Eip1559( + signed_tx_2.as_eip1559().expect("eip1559 transaction").clone(), + ); + let tx_hash_2 = tx_2.tx_hash(); + + let block = create_block_with_transactions(&harness, vec![tx_1, tx_2]); + + let state_provider = harness + .provider + .state_by_block_hash(harness.header.hash()) + .context("getting state provider")?; + + let response = + meter_block(state_provider, harness.chain_spec.clone(), &block, &harness.header)?; + + assert_eq!(response.block_hash, block.header().hash_slow()); + assert_eq!(response.block_number, block.header().number()); + assert_eq!(response.transactions.len(), 2); + + // Check first transaction + let metered_tx_1 = &response.transactions[0]; + assert_eq!(metered_tx_1.tx_hash, tx_hash_1); + assert_eq!(metered_tx_1.gas_used, 21_000); + assert!(metered_tx_1.execution_time_us > 0); + + // Check second transaction + let metered_tx_2 = &response.transactions[1]; + assert_eq!(metered_tx_2.tx_hash, tx_hash_2); + assert_eq!(metered_tx_2.gas_used, 21_000); + assert!(metered_tx_2.execution_time_us > 0); + + // Check aggregate times + assert!(response.execution_time_us > 0); + assert!(response.state_root_time_us > 0); + assert_eq!(response.total_time_us, response.execution_time_us + response.state_root_time_us); + + // Ensure individual transaction times are consistent with total + let individual_times: u128 = response.transactions.iter().map(|t| t.execution_time_us).sum(); + assert!( + individual_times <= response.execution_time_us, + "sum of individual times should not exceed total (due to EVM overhead)" + ); + + Ok(()) +} + +#[test] +fn meter_block_timing_consistency() -> eyre::Result<()> { + let harness = setup_harness()?; + + // Create a block with one transaction + let signed_tx = TransactionBuilder::default() + .signer(harness.signer(User::Alice)) + .chain_id(harness.chain_spec.chain_id()) + .nonce(0) + .to(Address::random()) + .value(1_000) + .gas_limit(21_000) + .max_fee_per_gas(10) + .max_priority_fee_per_gas(1) + .into_eip1559(); + + let tx = + OpTransactionSigned::Eip1559(signed_tx.as_eip1559().expect("eip1559 transaction").clone()); + + let block = create_block_with_transactions(&harness, vec![tx]); + + let state_provider = harness + .provider + .state_by_block_hash(harness.header.hash()) + .context("getting state provider")?; + + let response = + meter_block(state_provider, harness.chain_spec.clone(), &block, &harness.header)?; + + // Verify timing invariants + assert!(response.execution_time_us > 0, "execution time must be positive"); + assert!(response.state_root_time_us > 0, "state root time must be positive"); + assert_eq!( + response.total_time_us, + response.execution_time_us + response.state_root_time_us, + "total time must equal execution + state root times" + ); + + Ok(()) +} diff --git a/crates/metering/src/tests/meter.rs b/crates/metering/src/tests/bundle.rs similarity index 100% rename from crates/metering/src/tests/meter.rs rename to crates/metering/src/tests/bundle.rs diff --git a/crates/metering/src/tests/mod.rs b/crates/metering/src/tests/mod.rs index 80d28813..6de9c490 100644 --- a/crates/metering/src/tests/mod.rs +++ b/crates/metering/src/tests/mod.rs @@ -1,5 +1,7 @@ #[cfg(test)] -mod meter; +mod block; +#[cfg(test)] +mod bundle; #[cfg(test)] mod rpc; #[cfg(test)] diff --git a/crates/metering/src/tests/rpc.rs b/crates/metering/src/tests/rpc.rs index b77f9534..9d80176e 100644 --- a/crates/metering/src/tests/rpc.rs +++ b/crates/metering/src/tests/rpc.rs @@ -2,11 +2,11 @@ mod tests { use std::{any::Any, net::SocketAddr, sync::Arc}; - use alloy_eips::Encodable2718; + use alloy_eips::{BlockNumberOrTag, Encodable2718}; use alloy_genesis::Genesis; - use alloy_primitives::{Bytes, U256, address, b256, bytes}; + use alloy_primitives::{B256, Bytes, U256, address, b256, bytes}; use alloy_rpc_client::RpcClient; - use base_reth_test_utils::tracing::init_silenced_tracing; + use base_reth_test_utils::{harness::TestHarness, tracing::init_silenced_tracing}; use op_alloy_consensus::OpTxEnvelope; use reth::{ args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}, @@ -24,6 +24,7 @@ mod tests { use tips_core::types::Bundle; use crate::rpc::{MeteringApiImpl, MeteringApiServer}; + use crate::types::MeterBlockResponse; pub struct NodeContext { http_api_addr: SocketAddr, @@ -106,6 +107,21 @@ mod tests { }) } + /// Sets up a TestHarness with the metering RPC module enabled + async fn setup_harness_with_metering() -> eyre::Result { + TestHarness::with_launcher(|builder| async move { + builder + .extend_rpc_modules(|ctx| { + let metering_api = MeteringApiImpl::new(ctx.provider().clone()); + ctx.modules.merge_configured(metering_api.into_rpc())?; + Ok(()) + }) + .launch() + .await + }) + .await + } + #[tokio::test] async fn test_meter_bundle_empty() -> eyre::Result<()> { let node = setup_node().await?; @@ -423,4 +439,121 @@ mod tests { Ok(()) } + + // ======================= Block Metering RPC Tests ======================= + + #[tokio::test] + async fn test_meter_block_by_number() -> eyre::Result<()> { + let harness = setup_harness_with_metering().await?; + + // Build a block (block 1) so we have a non-genesis block to meter + harness.advance_chain(1).await?; + + let client = RpcClient::new_http(harness.rpc_url().parse()?); + + // Meter block 1 + let response: MeterBlockResponse = client + .request("base_meterBlockByNumber", (BlockNumberOrTag::Number(1),)) + .await?; + + assert_eq!(response.block_number, 1); + // Block 1 contains the L1 block info deposit transaction + assert!(!response.transactions.is_empty()); + assert!(response.execution_time_us > 0, "execution time should be non-zero"); + assert!(response.state_root_time_us > 0, "state root time should be non-zero"); + assert_eq!(response.total_time_us, response.execution_time_us + response.state_root_time_us); + + Ok(()) + } + + #[tokio::test] + async fn test_meter_block_by_number_latest() -> eyre::Result<()> { + let harness = setup_harness_with_metering().await?; + + // Build a few blocks + harness.advance_chain(3).await?; + + let client = RpcClient::new_http(harness.rpc_url().parse()?); + + // Meter the latest block + let response: MeterBlockResponse = client + .request("base_meterBlockByNumber", (BlockNumberOrTag::Latest,)) + .await?; + + // Latest block should be block 3 + assert_eq!(response.block_number, 3); + assert!(response.execution_time_us > 0); + assert!(response.state_root_time_us > 0); + + Ok(()) + } + + #[tokio::test] + async fn test_meter_block_by_hash() -> eyre::Result<()> { + let harness = setup_harness_with_metering().await?; + + // Build a block + harness.advance_chain(1).await?; + + // Get block hash from the latest block (which is block 1) + let block = harness.latest_block(); + let block_hash = block.hash(); + + let client = RpcClient::new_http(harness.rpc_url().parse()?); + + // Meter by hash + let response: MeterBlockResponse = + client.request("base_meterBlockByHash", (block_hash,)).await?; + + assert_eq!(response.block_hash, block_hash); + assert_eq!(response.block_number, 1); + assert!(response.execution_time_us > 0); + assert!(response.state_root_time_us > 0); + + Ok(()) + } + + #[tokio::test] + async fn test_meter_block_by_hash_not_found() -> eyre::Result<()> { + let harness = setup_harness_with_metering().await?; + let client = RpcClient::new_http(harness.rpc_url().parse()?); + + // Try to meter a non-existent block hash + let fake_hash = B256::random(); + + let result: Result = + client.request("base_meterBlockByHash", (fake_hash,)).await; + + assert!(result.is_err(), "should return error for non-existent block"); + + Ok(()) + } + + #[tokio::test] + async fn test_meter_block_by_number_not_found() -> eyre::Result<()> { + let harness = setup_harness_with_metering().await?; + let client = RpcClient::new_http(harness.rpc_url().parse()?); + + // Try to meter a block number that doesn't exist + let result: Result = + client.request("base_meterBlockByNumber", (BlockNumberOrTag::Number(999999),)).await; + + assert!(result.is_err(), "should return error for non-existent block number"); + + Ok(()) + } + + #[tokio::test] + async fn test_meter_block_genesis_fails() -> eyre::Result<()> { + let harness = setup_harness_with_metering().await?; + let client = RpcClient::new_http(harness.rpc_url().parse()?); + + // Genesis block (block 0) has no parent, so metering should fail + let result: Result = + client.request("base_meterBlockByNumber", (BlockNumberOrTag::Number(0),)).await; + + assert!(result.is_err(), "genesis block should fail because it has no parent"); + + Ok(()) + } } From 468eb75477804d4bc02dcde667d064f655217aaf Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Thu, 11 Dec 2025 17:32:05 -0500 Subject: [PATCH 4/7] feat: measure signer recovery time separately from execution Signer recovery can be parallelized, so it should be measured separately from EVM execution time. This adds signer_recovery_time_us to MeterBlockResponse and moves signer recovery before the execution loop. --- crates/metering/src/block.rs | 28 ++++++++++++++++++---------- crates/metering/src/tests/block.rs | 24 +++++++++++++++++++----- crates/metering/src/tests/rpc.rs | 6 +++++- crates/metering/src/types.rs | 4 +++- 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/crates/metering/src/block.rs b/crates/metering/src/block.rs index cf613ece..4834ea92 100644 --- a/crates/metering/src/block.rs +++ b/crates/metering/src/block.rs @@ -51,6 +51,20 @@ where extra_data: block.header().extra_data().clone(), }; + // Recover signers first (this can be parallelized in production) + let signer_recovery_start = Instant::now(); + let recovered_transactions: Vec<_> = transactions + .iter() + .map(|tx| { + let tx_hash = tx.tx_hash(); + let signer = tx + .recover_signer() + .map_err(|e| eyre!("Failed to recover signer for tx {}: {}", tx_hash, e))?; + Ok(alloy_consensus::transaction::Recovered::new_unchecked(tx.clone(), signer)) + }) + .collect::>>()?; + let signer_recovery_time = signer_recovery_start.elapsed().as_micros(); + // Execute transactions and measure time let mut transaction_times = Vec::with_capacity(tx_count); @@ -61,16 +75,9 @@ where builder.apply_pre_execution_changes()?; - for tx in &transactions { + for recovered_tx in recovered_transactions { let tx_start = Instant::now(); - let tx_hash = tx.tx_hash(); - - // Recover the signer to create a Recovered transaction for execution - let signer = tx - .recover_signer() - .map_err(|e| eyre!("Failed to recover signer for tx {}: {}", tx_hash, e))?; - let recovered_tx = - alloy_consensus::transaction::Recovered::new_unchecked(tx.clone(), signer); + let tx_hash = recovered_tx.tx_hash(); let gas_used = builder .execute_transaction(recovered_tx) @@ -96,11 +103,12 @@ where .map_err(|e| eyre!("Failed to calculate state root: {}", e))?; let state_root_time = state_root_start.elapsed().as_micros(); - let total_time = execution_time + state_root_time; + let total_time = signer_recovery_time + execution_time + state_root_time; Ok(MeterBlockResponse { block_hash, block_number, + signer_recovery_time_us: signer_recovery_time, execution_time_us: execution_time, state_root_time_us: state_root_time, total_time_us: total_time, diff --git a/crates/metering/src/tests/block.rs b/crates/metering/src/tests/block.rs index 005490f5..4f79f6bc 100644 --- a/crates/metering/src/tests/block.rs +++ b/crates/metering/src/tests/block.rs @@ -123,9 +123,14 @@ fn meter_block_empty_transactions() -> eyre::Result<()> { assert_eq!(response.block_hash, block.header().hash_slow()); assert_eq!(response.block_number, block.header().number()); assert!(response.transactions.is_empty()); + // No transactions means no signer recovery + assert_eq!(response.signer_recovery_time_us, 0); assert!(response.execution_time_us > 0, "execution time should be non-zero due to EVM setup"); assert!(response.state_root_time_us > 0, "state root time should be non-zero"); - assert_eq!(response.total_time_us, response.execution_time_us + response.state_root_time_us); + assert_eq!( + response.total_time_us, + response.signer_recovery_time_us + response.execution_time_us + response.state_root_time_us + ); Ok(()) } @@ -169,9 +174,13 @@ fn meter_block_single_transaction() -> eyre::Result<()> { assert_eq!(metered_tx.gas_used, 21_000); assert!(metered_tx.execution_time_us > 0, "transaction execution time should be non-zero"); + assert!(response.signer_recovery_time_us > 0, "signer recovery should take time"); assert!(response.execution_time_us > 0); assert!(response.state_root_time_us > 0); - assert_eq!(response.total_time_us, response.execution_time_us + response.state_root_time_us); + assert_eq!( + response.total_time_us, + response.signer_recovery_time_us + response.execution_time_us + response.state_root_time_us + ); Ok(()) } @@ -244,9 +253,13 @@ fn meter_block_multiple_transactions() -> eyre::Result<()> { assert!(metered_tx_2.execution_time_us > 0); // Check aggregate times + assert!(response.signer_recovery_time_us > 0, "signer recovery should take time"); assert!(response.execution_time_us > 0); assert!(response.state_root_time_us > 0); - assert_eq!(response.total_time_us, response.execution_time_us + response.state_root_time_us); + assert_eq!( + response.total_time_us, + response.signer_recovery_time_us + response.execution_time_us + response.state_root_time_us + ); // Ensure individual transaction times are consistent with total let individual_times: u128 = response.transactions.iter().map(|t| t.execution_time_us).sum(); @@ -288,12 +301,13 @@ fn meter_block_timing_consistency() -> eyre::Result<()> { meter_block(state_provider, harness.chain_spec.clone(), &block, &harness.header)?; // Verify timing invariants + assert!(response.signer_recovery_time_us > 0, "signer recovery time must be positive"); assert!(response.execution_time_us > 0, "execution time must be positive"); assert!(response.state_root_time_us > 0, "state root time must be positive"); assert_eq!( response.total_time_us, - response.execution_time_us + response.state_root_time_us, - "total time must equal execution + state root times" + response.signer_recovery_time_us + response.execution_time_us + response.state_root_time_us, + "total time must equal signer recovery + execution + state root times" ); Ok(()) diff --git a/crates/metering/src/tests/rpc.rs b/crates/metering/src/tests/rpc.rs index 9d80176e..877571ba 100644 --- a/crates/metering/src/tests/rpc.rs +++ b/crates/metering/src/tests/rpc.rs @@ -459,9 +459,13 @@ mod tests { assert_eq!(response.block_number, 1); // Block 1 contains the L1 block info deposit transaction assert!(!response.transactions.is_empty()); + assert!(response.signer_recovery_time_us > 0, "signer recovery time should be non-zero"); assert!(response.execution_time_us > 0, "execution time should be non-zero"); assert!(response.state_root_time_us > 0, "state root time should be non-zero"); - assert_eq!(response.total_time_us, response.execution_time_us + response.state_root_time_us); + assert_eq!( + response.total_time_us, + response.signer_recovery_time_us + response.execution_time_us + response.state_root_time_us + ); Ok(()) } diff --git a/crates/metering/src/types.rs b/crates/metering/src/types.rs index 1dc625a9..facf1813 100644 --- a/crates/metering/src/types.rs +++ b/crates/metering/src/types.rs @@ -12,11 +12,13 @@ pub struct MeterBlockResponse { pub block_hash: B256, /// The block number that was metered pub block_number: u64, + /// Duration of signer recovery in microseconds (can be parallelized) + pub signer_recovery_time_us: u128, /// Duration of EVM execution in microseconds pub execution_time_us: u128, /// Duration of state root calculation in microseconds pub state_root_time_us: u128, - /// Total duration (EVM execution + state root calculation) in microseconds + /// Total duration (signer recovery + EVM execution + state root calculation) in microseconds pub total_time_us: u128, /// Per-transaction metering data pub transactions: Vec, From 67b1fccc76e2f2062f23a081c5405b5f24275def Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Thu, 11 Dec 2025 17:59:36 -0500 Subject: [PATCH 5/7] docs: document state root timing caveats for block metering Add notes about: - Pruned parent state will return an error - Older blocks may have slower state root calculation due to uncached trie nodes --- crates/metering/src/block.rs | 9 +++++++++ crates/metering/src/types.rs | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/metering/src/block.rs b/crates/metering/src/block.rs index 4834ea92..2548b896 100644 --- a/crates/metering/src/block.rs +++ b/crates/metering/src/block.rs @@ -19,10 +19,19 @@ use crate::types::{MeterBlockResponse, MeterBlockTransactions}; /// /// Returns `MeterBlockResponse` containing: /// - Block hash +/// - Signer recovery time (can be parallelized) /// - EVM execution time for all transactions /// - State root calculation time /// - Total time /// - Per-transaction timing information +/// +/// # Note +/// +/// If the parent block's state has been pruned, this function will return an error. +/// +/// State root calculation timing is most accurate for recent blocks where state tries are +/// cached. For older blocks, trie nodes may not be cached, which can significantly inflate +/// the `state_root_time_us` value. pub fn meter_block( state_provider: SP, chain_spec: Arc, diff --git a/crates/metering/src/types.rs b/crates/metering/src/types.rs index facf1813..edb91a96 100644 --- a/crates/metering/src/types.rs +++ b/crates/metering/src/types.rs @@ -16,7 +16,10 @@ pub struct MeterBlockResponse { pub signer_recovery_time_us: u128, /// Duration of EVM execution in microseconds pub execution_time_us: u128, - /// Duration of state root calculation in microseconds + /// Duration of state root calculation in microseconds. + /// + /// Note: This timing is most accurate for recent blocks where state tries are cached. + /// For older blocks, trie nodes may not be cached, which can significantly inflate this value. pub state_root_time_us: u128, /// Total duration (signer recovery + EVM execution + state root calculation) in microseconds pub total_time_us: u128, From 9da0d2931a835f1b8070a375b81411302f036ec1 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Thu, 11 Dec 2025 17:59:46 -0500 Subject: [PATCH 6/7] test: fix empty block signer recovery time assertion Remove strict equality check for signer recovery time on empty blocks since timing overhead can result in non-zero values. --- crates/metering/src/tests/block.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/metering/src/tests/block.rs b/crates/metering/src/tests/block.rs index 4f79f6bc..2f98dc87 100644 --- a/crates/metering/src/tests/block.rs +++ b/crates/metering/src/tests/block.rs @@ -123,8 +123,7 @@ fn meter_block_empty_transactions() -> eyre::Result<()> { assert_eq!(response.block_hash, block.header().hash_slow()); assert_eq!(response.block_number, block.header().number()); assert!(response.transactions.is_empty()); - // No transactions means no signer recovery - assert_eq!(response.signer_recovery_time_us, 0); + // No transactions means minimal signer recovery time (just timing overhead) assert!(response.execution_time_us > 0, "execution time should be non-zero due to EVM setup"); assert!(response.state_root_time_us > 0, "state root time should be non-zero"); assert_eq!( From 90be6106229bb4811b2d5a9c35b699c598a036f2 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Thu, 11 Dec 2025 18:17:25 -0500 Subject: [PATCH 7/7] refactor: simplify meter_block API by accepting provider directly Instead of requiring the caller to pass a state provider and parent header separately, meter_block now accepts a provider that implements both StateProviderFactory and HeaderProvider. This allows the function to fetch the parent header and state internally, resulting in a cleaner API. --- crates/metering/src/block.rs | 26 +++++++++----- crates/metering/src/rpc.rs | 40 +++------------------- crates/metering/src/tests/block.rs | 55 +++++++++++------------------- 3 files changed, 41 insertions(+), 80 deletions(-) diff --git a/crates/metering/src/block.rs b/crates/metering/src/block.rs index 2548b896..550ea88e 100644 --- a/crates/metering/src/block.rs +++ b/crates/metering/src/block.rs @@ -1,6 +1,6 @@ use std::{sync::Arc, time::Instant}; -use alloy_consensus::{BlockHeader, transaction::SignerRecoverable}; +use alloy_consensus::{BlockHeader, Header, transaction::SignerRecoverable}; use alloy_primitives::B256; use eyre::{Result as EyreResult, eyre}; use reth::revm::db::State; @@ -8,14 +8,14 @@ use reth_evm::{ConfigureEvm, execute::BlockBuilder}; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; use reth_optimism_primitives::OpBlock; -use reth_primitives_traits::{Block as BlockT, SealedHeader}; -use reth_provider::{HashedPostStateProvider, StateRootProvider}; +use reth_primitives_traits::Block as BlockT; +use reth_provider::{HeaderProvider, StateProviderFactory}; use crate::types::{MeterBlockResponse, MeterBlockTransactions}; /// Re-executes a block and meters execution time, state root calculation time, and total time. /// -/// Takes a state provider at the parent block, the chain spec, and the block to meter. +/// Takes a provider, the chain spec, and the block to meter. /// /// Returns `MeterBlockResponse` containing: /// - Block hash @@ -32,20 +32,28 @@ use crate::types::{MeterBlockResponse, MeterBlockTransactions}; /// State root calculation timing is most accurate for recent blocks where state tries are /// cached. For older blocks, trie nodes may not be cached, which can significantly inflate /// the `state_root_time_us` value. -pub fn meter_block( - state_provider: SP, +pub fn meter_block

( + provider: P, chain_spec: Arc, block: &OpBlock, - parent_header: &SealedHeader, ) -> EyreResult where - SP: reth_provider::StateProvider + StateRootProvider + HashedPostStateProvider, + P: StateProviderFactory + HeaderProvider

, { let block_hash = block.header().hash_slow(); let block_number = block.header().number(); let transactions: Vec<_> = block.body().transactions().cloned().collect(); let tx_count = transactions.len(); + // Get parent header + let parent_hash = block.header().parent_hash(); + let parent_header = provider + .sealed_header_by_hash(parent_hash)? + .ok_or_else(|| eyre!("Parent header not found: {}", parent_hash))?; + + // Get state provider at parent block + let state_provider = provider.state_by_block_hash(parent_hash)?; + // Create state database from parent state let state_db = reth::revm::database::StateProviderDatabase::new(&state_provider); let mut db = State::builder().with_database(state_db).with_bundle_update().build(); @@ -80,7 +88,7 @@ where let evm_start = Instant::now(); { let evm_config = OpEvmConfig::optimism(chain_spec); - let mut builder = evm_config.builder_for_next_block(&mut db, parent_header, attributes)?; + let mut builder = evm_config.builder_for_next_block(&mut db, &parent_header, attributes)?; builder.apply_pre_execution_changes()?; diff --git a/crates/metering/src/rpc.rs b/crates/metering/src/rpc.rs index c197a44e..d9665fb3 100644 --- a/crates/metering/src/rpc.rs +++ b/crates/metering/src/rpc.rs @@ -8,7 +8,6 @@ use jsonrpsee::{ use reth::providers::BlockReaderIdExt; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_primitives::OpBlock; -use reth_primitives_traits::Block as BlockT; use reth_provider::{BlockReader, ChainSpecProvider, StateProviderFactory}; use tips_core::types::{Bundle, MeterBundleResponse, ParsedBundle}; use tracing::{error, info}; @@ -261,46 +260,15 @@ where { /// Internal helper to meter a block's execution fn meter_block_internal(&self, block: &OpBlock) -> RpcResult { - // Get parent header - let parent_hash = block.header().parent_hash; - let parent_header = self - .provider - .sealed_header_by_hash(parent_hash) - .map_err(|e| { - error!(error = %e, "Failed to get parent header"); - jsonrpsee::types::ErrorObjectOwned::owned( - jsonrpsee::types::ErrorCode::InternalError.code(), - format!("Failed to get parent header: {}", e), - None::<()>, - ) - })? - .ok_or_else(|| { - jsonrpsee::types::ErrorObjectOwned::owned( - jsonrpsee::types::ErrorCode::InternalError.code(), - format!("Parent block not found: {}", parent_hash), - None::<()>, - ) - })?; - - // Get state provider at parent block - let state_provider = self.provider.state_by_block_hash(parent_hash).map_err(|e| { - error!(error = %e, "Failed to get state provider for parent block"); - jsonrpsee::types::ErrorObjectOwned::owned( - jsonrpsee::types::ErrorCode::InternalError.code(), - format!("Failed to get state provider: {}", e), - None::<()>, - ) - })?; - - // Meter the block - meter_block(state_provider, self.provider.chain_spec().clone(), block, &parent_header) - .map_err(|e| { + meter_block(self.provider.clone(), self.provider.chain_spec().clone(), block).map_err( + |e| { error!(error = %e, "Block metering failed"); jsonrpsee::types::ErrorObjectOwned::owned( jsonrpsee::types::ErrorCode::InternalError.code(), format!("Block metering failed: {}", e), None::<()>, ) - }) + }, + ) } } diff --git a/crates/metering/src/tests/block.rs b/crates/metering/src/tests/block.rs index 2f98dc87..a9a623d1 100644 --- a/crates/metering/src/tests/block.rs +++ b/crates/metering/src/tests/block.rs @@ -10,8 +10,8 @@ use reth_db::{DatabaseEnv, test_utils::TempDatabase}; use reth_optimism_chainspec::{BASE_MAINNET, OpChainSpec, OpChainSpecBuilder}; use reth_optimism_node::OpNode; use reth_optimism_primitives::{OpBlock, OpBlockBody, OpTransactionSigned}; -use reth_primitives_traits::{Block as BlockT, SealedHeader}; -use reth_provider::{HeaderProvider, StateProviderFactory, providers::BlockchainProvider}; +use reth_primitives_traits::Block as BlockT; +use reth_provider::{HeaderProvider, providers::BlockchainProvider}; use reth_testing_utils::generators::generate_keys; use reth_transaction_pool::test_utils::TransactionBuilder; @@ -29,7 +29,9 @@ enum User { #[derive(Debug, Clone)] struct TestHarness { provider: BlockchainProvider, - header: SealedHeader, + genesis_header_hash: B256, + genesis_header_number: u64, + genesis_header_timestamp: u64, chain_spec: Arc, user_to_private_key: std::collections::HashMap, } @@ -82,7 +84,14 @@ fn setup_harness() -> eyre::Result { .context("fetching genesis header")? .expect("genesis header exists"); - Ok(TestHarness { provider, header, chain_spec, user_to_private_key }) + Ok(TestHarness { + provider, + genesis_header_hash: header.hash(), + genesis_header_number: header.number(), + genesis_header_timestamp: header.timestamp(), + chain_spec, + user_to_private_key, + }) } fn create_block_with_transactions( @@ -90,9 +99,9 @@ fn create_block_with_transactions( transactions: Vec, ) -> OpBlock { let header = Header { - parent_hash: harness.header.hash(), - number: harness.header.number() + 1, - timestamp: harness.header.timestamp() + 2, + parent_hash: harness.genesis_header_hash, + number: harness.genesis_header_number + 1, + timestamp: harness.genesis_header_timestamp + 2, gas_limit: 30_000_000, beneficiary: Address::random(), base_fee_per_gas: Some(1), @@ -112,13 +121,7 @@ fn meter_block_empty_transactions() -> eyre::Result<()> { let block = create_block_with_transactions(&harness, vec![]); - let state_provider = harness - .provider - .state_by_block_hash(harness.header.hash()) - .context("getting state provider")?; - - let response = - meter_block(state_provider, harness.chain_spec.clone(), &block, &harness.header)?; + let response = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block)?; assert_eq!(response.block_hash, block.header().hash_slow()); assert_eq!(response.block_number, block.header().number()); @@ -156,13 +159,7 @@ fn meter_block_single_transaction() -> eyre::Result<()> { let block = create_block_with_transactions(&harness, vec![tx]); - let state_provider = harness - .provider - .state_by_block_hash(harness.header.hash()) - .context("getting state provider")?; - - let response = - meter_block(state_provider, harness.chain_spec.clone(), &block, &harness.header)?; + let response = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block)?; assert_eq!(response.block_hash, block.header().hash_slow()); assert_eq!(response.block_number, block.header().number()); @@ -227,13 +224,7 @@ fn meter_block_multiple_transactions() -> eyre::Result<()> { let block = create_block_with_transactions(&harness, vec![tx_1, tx_2]); - let state_provider = harness - .provider - .state_by_block_hash(harness.header.hash()) - .context("getting state provider")?; - - let response = - meter_block(state_provider, harness.chain_spec.clone(), &block, &harness.header)?; + let response = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block)?; assert_eq!(response.block_hash, block.header().hash_slow()); assert_eq!(response.block_number, block.header().number()); @@ -291,13 +282,7 @@ fn meter_block_timing_consistency() -> eyre::Result<()> { let block = create_block_with_transactions(&harness, vec![tx]); - let state_provider = harness - .provider - .state_by_block_hash(harness.header.hash()) - .context("getting state provider")?; - - let response = - meter_block(state_provider, harness.chain_spec.clone(), &block, &harness.header)?; + let response = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block)?; // Verify timing invariants assert!(response.signer_recovery_time_us > 0, "signer recovery time must be positive");