Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 134 additions & 0 deletions crates/rpc/src/base/block.rs
Original file line number Diff line number Diff line change
@@ -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<P>(
provider: P,
chain_spec: Arc<OpChainSpec>,
block: &OpBlock,
) -> EyreResult<MeterBlockResponse>
where
P: StateProviderFactory + HeaderProvider<Header = Header>,
{
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::<EyreResult<Vec<_>>>()?;
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,
})
}
114 changes: 111 additions & 3 deletions crates/rpc/src/base/meter_rpc.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -21,6 +24,8 @@ where
Provider: StateProviderFactory
+ ChainSpecProvider<ChainSpec = OpChainSpec>
+ BlockReaderIdExt<Header = Header>
+ BlockReader<Block = OpBlock>
+ HeaderProvider<Header = Header>
+ Clone,
{
/// Creates a new instance of MeteringApi
Expand All @@ -35,6 +40,8 @@ where
Provider: StateProviderFactory
+ ChainSpecProvider<ChainSpec = OpChainSpec>
+ BlockReaderIdExt<Header = Header>
+ BlockReader<Block = OpBlock>
+ HeaderProvider<Header = Header>
+ Clone
+ Send
+ Sync
Expand Down Expand Up @@ -124,4 +131,105 @@ where
total_execution_time_us: total_execution_time,
})
}

async fn meter_block_by_hash(&self, hash: B256) -> RpcResult<MeterBlockResponse> {
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<MeterBlockResponse> {
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<Provider> MeteringApiImpl<Provider>
where
Provider: StateProviderFactory
+ ChainSpecProvider<ChainSpec = OpChainSpec>
+ BlockReaderIdExt<Header = Header>
+ BlockReader<Block = OpBlock>
+ HeaderProvider<Header = Header>
+ Clone
+ Send
+ Sync
+ 'static,
{
/// Internal helper to meter a block's execution
fn meter_block_internal(&self, block: &OpBlock) -> RpcResult<MeterBlockResponse> {
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::<()>,
)
})
}
}
1 change: 1 addition & 0 deletions crates/rpc/src/base/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) mod block;
pub(crate) mod meter;
pub(crate) mod meter_rpc;
pub(crate) mod pubsub;
Expand Down
34 changes: 32 additions & 2 deletions crates/rpc/src/base/traits.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,46 @@
//! 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")]
pub trait MeteringApi {
/// Simulates and meters a bundle of transactions
#[method(name = "meterBundle")]
async fn meter_bundle(&self, bundle: Bundle) -> RpcResult<MeterBundleResponse>;

/// 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<MeterBlockResponse>;

/// 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<MeterBlockResponse>;
}

/// RPC API for transaction status
Expand Down
39 changes: 39 additions & 0 deletions crates/rpc/src/base/types.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -95,3 +96,41 @@ impl From<BaseSubscriptionKind> 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<MeterBlockTransactions>,
}

/// 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,
}
Loading