Skip to content
Open
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/metering/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jsonrpsee.workspace = true
# misc
tracing.workspace = true
eyre.workspace = true
serde.workspace = true

[dev-dependencies]
alloy-genesis.workspace = true
Expand Down
109 changes: 109 additions & 0 deletions crates/metering/src/block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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<SP>(
state_provider: SP,
chain_spec: Arc<OpChainSpec>,
block: &OpBlock,
parent_header: &SealedHeader,
) -> EyreResult<MeterBlockResponse>
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);
Comment on lines +69 to +73

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect signer recovery to not be part of the metered tx execution time. This is something that can always be parallelized, so we should probably record this separately.


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may depend heavily on the age of the block, as historical tries may need to be fully recomputed from state diffs.


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,
})
}
File renamed without changes.
8 changes: 6 additions & 2 deletions crates/metering/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
mod meter;
mod block;
mod bundle;
mod rpc;
#[cfg(test)]
mod tests;
mod types;

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};
170 changes: 167 additions & 3 deletions crates/metering/src/rpc.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,55 @@
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, bundle::meter_bundle, types::MeterBlockResponse};

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

/// Implementation of the metering RPC API
Expand All @@ -31,6 +62,7 @@ where
Provider: StateProviderFactory
+ ChainSpecProvider<ChainSpec = OpChainSpec>
+ BlockReaderIdExt<Header = Header>
+ BlockReader<Block = OpBlock>
+ Clone,
{
/// Creates a new instance of MeteringApi
Expand All @@ -45,6 +77,7 @@ where
Provider: StateProviderFactory
+ ChainSpecProvider<ChainSpec = OpChainSpec>
+ BlockReaderIdExt<Header = Header>
+ BlockReader<Block = OpBlock>
+ Clone
+ Send
+ Sync
Expand Down Expand Up @@ -139,4 +172,135 @@ 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,
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,
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>
+ Clone
+ Send
+ Sync
+ 'static,
{
/// Internal helper to meter a block's execution
fn meter_block_internal(&self, block: &OpBlock) -> RpcResult<MeterBlockResponse> {
// 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::<()>,
)
})
}
}
35 changes: 35 additions & 0 deletions crates/metering/src/types.rs
Original file line number Diff line number Diff line change
@@ -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<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,
}