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
134 changes: 134 additions & 0 deletions crates/metering/src/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 crate::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,
})
}
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};
138 changes: 135 additions & 3 deletions crates/metering/src/rpc.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
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_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 +61,7 @@ where
Provider: StateProviderFactory
+ ChainSpecProvider<ChainSpec = OpChainSpec>
+ BlockReaderIdExt<Header = Header>
+ BlockReader<Block = OpBlock>
+ Clone,
{
/// Creates a new instance of MeteringApi
Expand All @@ -45,6 +76,7 @@ where
Provider: StateProviderFactory
+ ChainSpecProvider<ChainSpec = OpChainSpec>
+ BlockReaderIdExt<Header = Header>
+ BlockReader<Block = OpBlock>
+ Clone
+ Send
+ Sync
Expand Down Expand Up @@ -139,4 +171,104 @@ 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> {
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::<()>,
)
},
)
}
}
Loading
Loading