diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 390fdf1a..a44ac84c 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -24,6 +24,7 @@ reth-primitives-traits.workspace = true reth-evm.workspace = true reth-optimism-evm.workspace = true reth-optimism-chainspec.workspace = true +reth-optimism-primitives.workspace = true reth-transaction-pool = { workspace = true, features = ["test-utils"] } reth-rpc.workspace = true reth-rpc-eth-api.workspace = true diff --git a/crates/rpc/src/base/block.rs b/crates/rpc/src/base/block.rs new file mode 100644 index 00000000..c1d04051 --- /dev/null +++ b/crates/rpc/src/base/block.rs @@ -0,0 +1,134 @@ +use std::{sync::Arc, time::Instant}; + +use alloy_consensus::{BlockHeader, Header, 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; +use reth_provider::{HeaderProvider, StateProviderFactory}; + +use super::types::{MeterBlockResponse, MeterBlockTransactions}; + +/// Re-executes a block and meters execution time, state root calculation time, and total time. +/// +/// Takes a provider, the chain spec, and the block to meter. +/// +/// 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

( + provider: P, + chain_spec: Arc, + block: &OpBlock, +) -> EyreResult +where + 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(); + + // 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(), + }; + + // 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); + + 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 recovered_tx in recovered_transactions { + let tx_start = Instant::now(); + let tx_hash = recovered_tx.tx_hash(); + + 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 = 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, + transactions: transaction_times, + }) +} diff --git a/crates/rpc/src/base/meter_rpc.rs b/crates/rpc/src/base/meter_rpc.rs index 9a99aa32..631d287f 100644 --- a/crates/rpc/src/base/meter_rpc.rs +++ b/crates/rpc/src/base/meter_rpc.rs @@ -1,14 +1,17 @@ use alloy_consensus::Header; use alloy_eips::BlockNumberOrTag; -use alloy_primitives::U256; +use alloy_primitives::{B256, U256}; use jsonrpsee::core::{RpcResult, async_trait}; use reth::providers::BlockReaderIdExt; use reth_optimism_chainspec::OpChainSpec; -use reth_provider::{ChainSpecProvider, StateProviderFactory}; +use reth_optimism_primitives::OpBlock; +use reth_provider::{BlockReader, ChainSpecProvider, HeaderProvider, StateProviderFactory}; use tips_core::types::{Bundle, MeterBundleResponse, ParsedBundle}; use tracing::{error, info}; -use crate::{MeteringApiServer, meter_bundle}; +use super::{ + block::meter_block, meter::meter_bundle, traits::MeteringApiServer, types::MeterBlockResponse, +}; /// Implementation of the metering RPC API #[derive(Debug)] @@ -21,6 +24,8 @@ where Provider: StateProviderFactory + ChainSpecProvider + BlockReaderIdExt
+ + BlockReader + + HeaderProvider
+ Clone, { /// Creates a new instance of MeteringApi @@ -35,6 +40,8 @@ where Provider: StateProviderFactory + ChainSpecProvider + BlockReaderIdExt
+ + BlockReader + + HeaderProvider
+ Clone + Send + Sync @@ -124,4 +131,105 @@ 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, + signer_recovery_time_us = response.signer_recovery_time_us, + 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, + signer_recovery_time_us = response.signer_recovery_time_us, + 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 + + HeaderProvider
+ + Clone + + Send + + Sync + + 'static, +{ + /// Internal helper to meter a block's execution + fn meter_block_internal(&self, block: &OpBlock) -> RpcResult { + meter_block(self.provider.clone(), self.provider.chain_spec(), 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/rpc/src/base/mod.rs b/crates/rpc/src/base/mod.rs index b4097c40..05aea14f 100644 --- a/crates/rpc/src/base/mod.rs +++ b/crates/rpc/src/base/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod block; pub(crate) mod meter; pub(crate) mod meter_rpc; pub(crate) mod pubsub; diff --git a/crates/rpc/src/base/traits.rs b/crates/rpc/src/base/traits.rs index 4f80de59..00ca4c1a 100644 --- a/crates/rpc/src/base/traits.rs +++ b/crates/rpc/src/base/traits.rs @@ -1,9 +1,10 @@ //! Traits for the RPC module. -use alloy_primitives::TxHash; +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::{B256, TxHash}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use crate::{Bundle, MeterBundleResponse, TransactionStatusResponse}; +use crate::{Bundle, MeterBlockResponse, MeterBundleResponse, TransactionStatusResponse}; /// RPC API for transaction metering #[rpc(server, namespace = "base")] @@ -11,6 +12,35 @@ 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; } /// RPC API for transaction status diff --git a/crates/rpc/src/base/types.rs b/crates/rpc/src/base/types.rs index 3340a80e..13b443a6 100644 --- a/crates/rpc/src/base/types.rs +++ b/crates/rpc/src/base/types.rs @@ -1,5 +1,6 @@ //! Types for the transaction status rpc +use alloy_primitives::B256; use alloy_rpc_types_eth::pubsub::SubscriptionKind; use serde::{Deserialize, Serialize}; @@ -95,3 +96,41 @@ impl From for ExtendedSubscriptionKind { Self::Base(kind) } } + +// Block metering types + +/// 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 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. + /// + /// 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, + /// 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, +} diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index 5a5db0a0..6f839e5d 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -8,12 +8,16 @@ pub use tips_core::types::{Bundle, MeterBundleResponse, TransactionResult}; mod base; pub use base::{ + block::meter_block, meter::meter_bundle, meter_rpc::MeteringApiImpl, pubsub::{EthPubSub, EthPubSubApiServer}, traits::{MeteringApiServer, TransactionStatusApiServer}, transaction_rpc::TransactionStatusApiImpl, - types::{BaseSubscriptionKind, ExtendedSubscriptionKind, Status, TransactionStatusResponse}, + types::{ + BaseSubscriptionKind, ExtendedSubscriptionKind, MeterBlockResponse, MeterBlockTransactions, + Status, TransactionStatusResponse, + }, }; mod eth; diff --git a/crates/rpc/tests/meter_block.rs b/crates/rpc/tests/meter_block.rs new file mode 100644 index 00000000..fc3b54b2 --- /dev/null +++ b/crates/rpc/tests/meter_block.rs @@ -0,0 +1,380 @@ +//! Integration tests for block metering functionality. + +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 base_reth_rpc::meter_block; +use base_reth_test_utils::create_provider_factory; +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; +use reth_provider::{HeaderProvider, providers::BlockchainProvider}; +use reth_testing_utils::generators::generate_keys; +use reth_transaction_pool::test_utils::TransactionBuilder; + +type NodeTypes = NodeTypesWithDBAdapter>>; + +#[derive(Eq, PartialEq, Debug, Hash, Clone, Copy)] +enum User { + Alice, + Bob, +} + +#[derive(Debug, Clone)] +struct TestHarness { + provider: BlockchainProvider, + genesis_header_hash: B256, + genesis_header_number: u64, + genesis_header_timestamp: u64, + 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, + 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( + harness: &TestHarness, + transactions: Vec, +) -> OpBlock { + let header = Header { + 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), + // 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 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()); + assert!(response.transactions.is_empty()); + // 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!( + response.total_time_us, + response.signer_recovery_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 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()); + 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.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.signer_recovery_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 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()); + 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.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.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(); + 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 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"); + 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.signer_recovery_time_us + response.execution_time_us + response.state_root_time_us, + "total time must equal signer recovery + execution + state root times" + ); + + Ok(()) +} + +// ============================================================================ +// Error Path Tests +// ============================================================================ + +#[test] +fn meter_block_parent_header_not_found() -> eyre::Result<()> { + let harness = setup_harness()?; + + // Create a block that references a non-existent parent + let fake_parent_hash = B256::random(); + let header = Header { + parent_hash: fake_parent_hash, // This parent doesn't exist + number: 999, + timestamp: harness.genesis_header_timestamp + 2, + gas_limit: 30_000_000, + beneficiary: Address::random(), + base_fee_per_gas: Some(1), + parent_beacon_block_root: Some(B256::ZERO), + ..Default::default() + }; + + let body = OpBlockBody { transactions: vec![], ommers: vec![], withdrawals: None }; + let block = OpBlock::new(header, body); + + let result = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block); + + assert!(result.is_err(), "should fail when parent header is not found"); + let err = result.unwrap_err(); + let err_str = err.to_string(); + assert!( + err_str.contains("Parent header not found") || err_str.contains("not found"), + "error should indicate parent header not found: {}", + err_str + ); + + Ok(()) +} + +#[test] +fn meter_block_invalid_transaction_signature() -> eyre::Result<()> { + use alloy_consensus::TxEip1559; + use alloy_primitives::Signature; + + let harness = setup_harness()?; + + // Create a transaction with an invalid signature + let tx = TxEip1559 { + chain_id: harness.chain_spec.chain_id(), + nonce: 0, + gas_limit: 21_000, + max_fee_per_gas: 10, + max_priority_fee_per_gas: 1, + to: alloy_primitives::TxKind::Call(Address::random()), + value: alloy_primitives::U256::from(1000), + access_list: Default::default(), + input: Default::default(), + }; + + // Create a signature with invalid values (all zeros is invalid for secp256k1) + let invalid_signature = + Signature::new(alloy_primitives::U256::ZERO, alloy_primitives::U256::ZERO, false); + + let signed_tx = alloy_consensus::Signed::new_unchecked(tx, invalid_signature, B256::random()); + let op_tx = OpTransactionSigned::Eip1559(signed_tx); + + let block = create_block_with_transactions(&harness, vec![op_tx]); + + let result = meter_block(harness.provider.clone(), harness.chain_spec.clone(), &block); + + assert!(result.is_err(), "should fail when transaction has invalid signature"); + let err = result.unwrap_err(); + let err_str = err.to_string(); + assert!( + err_str.contains("recover signer") || err_str.contains("signature"), + "error should indicate signer recovery failure: {}", + err_str + ); + + Ok(()) +}