Skip to content

Commit 328e8aa

Browse files
authored
feat: add block metering RPC endpoints (#216)
* 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 * style: apply rustfmt formatting * 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. * 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. * 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 * 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. * 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. * test: add error path tests for block metering Add tests for missing parent header and invalid transaction signature error cases to ensure proper error handling in meter_block. * style: apply rustfmt and fix clippy warning Remove redundant clone in meter_block_internal.
1 parent 7e8db37 commit 328e8aa

File tree

8 files changed

+703
-6
lines changed

8 files changed

+703
-6
lines changed

crates/rpc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ reth-primitives-traits.workspace = true
2424
reth-evm.workspace = true
2525
reth-optimism-evm.workspace = true
2626
reth-optimism-chainspec.workspace = true
27+
reth-optimism-primitives.workspace = true
2728
reth-transaction-pool = { workspace = true, features = ["test-utils"] }
2829
reth-rpc.workspace = true
2930
reth-rpc-eth-api.workspace = true

crates/rpc/src/base/block.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use std::{sync::Arc, time::Instant};
2+
3+
use alloy_consensus::{BlockHeader, Header, transaction::SignerRecoverable};
4+
use alloy_primitives::B256;
5+
use eyre::{Result as EyreResult, eyre};
6+
use reth::revm::db::State;
7+
use reth_evm::{ConfigureEvm, execute::BlockBuilder};
8+
use reth_optimism_chainspec::OpChainSpec;
9+
use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes};
10+
use reth_optimism_primitives::OpBlock;
11+
use reth_primitives_traits::Block as BlockT;
12+
use reth_provider::{HeaderProvider, StateProviderFactory};
13+
14+
use super::types::{MeterBlockResponse, MeterBlockTransactions};
15+
16+
/// Re-executes a block and meters execution time, state root calculation time, and total time.
17+
///
18+
/// Takes a provider, the chain spec, and the block to meter.
19+
///
20+
/// Returns `MeterBlockResponse` containing:
21+
/// - Block hash
22+
/// - Signer recovery time (can be parallelized)
23+
/// - EVM execution time for all transactions
24+
/// - State root calculation time
25+
/// - Total time
26+
/// - Per-transaction timing information
27+
///
28+
/// # Note
29+
///
30+
/// If the parent block's state has been pruned, this function will return an error.
31+
///
32+
/// State root calculation timing is most accurate for recent blocks where state tries are
33+
/// cached. For older blocks, trie nodes may not be cached, which can significantly inflate
34+
/// the `state_root_time_us` value.
35+
pub fn meter_block<P>(
36+
provider: P,
37+
chain_spec: Arc<OpChainSpec>,
38+
block: &OpBlock,
39+
) -> EyreResult<MeterBlockResponse>
40+
where
41+
P: StateProviderFactory + HeaderProvider<Header = Header>,
42+
{
43+
let block_hash = block.header().hash_slow();
44+
let block_number = block.header().number();
45+
let transactions: Vec<_> = block.body().transactions().cloned().collect();
46+
let tx_count = transactions.len();
47+
48+
// Get parent header
49+
let parent_hash = block.header().parent_hash();
50+
let parent_header = provider
51+
.sealed_header_by_hash(parent_hash)?
52+
.ok_or_else(|| eyre!("Parent header not found: {}", parent_hash))?;
53+
54+
// Get state provider at parent block
55+
let state_provider = provider.state_by_block_hash(parent_hash)?;
56+
57+
// Create state database from parent state
58+
let state_db = reth::revm::database::StateProviderDatabase::new(&state_provider);
59+
let mut db = State::builder().with_database(state_db).with_bundle_update().build();
60+
61+
// Set up block attributes from the actual block header
62+
let attributes = OpNextBlockEnvAttributes {
63+
timestamp: block.header().timestamp(),
64+
suggested_fee_recipient: block.header().beneficiary(),
65+
prev_randao: block.header().mix_hash().unwrap_or(B256::random()),
66+
gas_limit: block.header().gas_limit(),
67+
parent_beacon_block_root: block.header().parent_beacon_block_root(),
68+
extra_data: block.header().extra_data().clone(),
69+
};
70+
71+
// Recover signers first (this can be parallelized in production)
72+
let signer_recovery_start = Instant::now();
73+
let recovered_transactions: Vec<_> = transactions
74+
.iter()
75+
.map(|tx| {
76+
let tx_hash = tx.tx_hash();
77+
let signer = tx
78+
.recover_signer()
79+
.map_err(|e| eyre!("Failed to recover signer for tx {}: {}", tx_hash, e))?;
80+
Ok(alloy_consensus::transaction::Recovered::new_unchecked(tx.clone(), signer))
81+
})
82+
.collect::<EyreResult<Vec<_>>>()?;
83+
let signer_recovery_time = signer_recovery_start.elapsed().as_micros();
84+
85+
// Execute transactions and measure time
86+
let mut transaction_times = Vec::with_capacity(tx_count);
87+
88+
let evm_start = Instant::now();
89+
{
90+
let evm_config = OpEvmConfig::optimism(chain_spec);
91+
let mut builder = evm_config.builder_for_next_block(&mut db, &parent_header, attributes)?;
92+
93+
builder.apply_pre_execution_changes()?;
94+
95+
for recovered_tx in recovered_transactions {
96+
let tx_start = Instant::now();
97+
let tx_hash = recovered_tx.tx_hash();
98+
99+
let gas_used = builder
100+
.execute_transaction(recovered_tx)
101+
.map_err(|e| eyre!("Transaction {} execution failed: {}", tx_hash, e))?;
102+
103+
let execution_time = tx_start.elapsed().as_micros();
104+
105+
transaction_times.push(MeterBlockTransactions {
106+
tx_hash,
107+
gas_used,
108+
execution_time_us: execution_time,
109+
});
110+
}
111+
}
112+
let execution_time = evm_start.elapsed().as_micros();
113+
114+
// Calculate state root and measure time
115+
let state_root_start = Instant::now();
116+
let bundle_state = db.bundle_state.clone();
117+
let hashed_state = state_provider.hashed_post_state(&bundle_state);
118+
let _state_root = state_provider
119+
.state_root(hashed_state)
120+
.map_err(|e| eyre!("Failed to calculate state root: {}", e))?;
121+
let state_root_time = state_root_start.elapsed().as_micros();
122+
123+
let total_time = signer_recovery_time + execution_time + state_root_time;
124+
125+
Ok(MeterBlockResponse {
126+
block_hash,
127+
block_number,
128+
signer_recovery_time_us: signer_recovery_time,
129+
execution_time_us: execution_time,
130+
state_root_time_us: state_root_time,
131+
total_time_us: total_time,
132+
transactions: transaction_times,
133+
})
134+
}

crates/rpc/src/base/meter_rpc.rs

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
use alloy_consensus::Header;
22
use alloy_eips::BlockNumberOrTag;
3-
use alloy_primitives::U256;
3+
use alloy_primitives::{B256, U256};
44
use jsonrpsee::core::{RpcResult, async_trait};
55
use reth::providers::BlockReaderIdExt;
66
use reth_optimism_chainspec::OpChainSpec;
7-
use reth_provider::{ChainSpecProvider, StateProviderFactory};
7+
use reth_optimism_primitives::OpBlock;
8+
use reth_provider::{BlockReader, ChainSpecProvider, HeaderProvider, StateProviderFactory};
89
use tips_core::types::{Bundle, MeterBundleResponse, ParsedBundle};
910
use tracing::{error, info};
1011

11-
use crate::{MeteringApiServer, meter_bundle};
12+
use super::{
13+
block::meter_block, meter::meter_bundle, traits::MeteringApiServer, types::MeterBlockResponse,
14+
};
1215

1316
/// Implementation of the metering RPC API
1417
#[derive(Debug)]
@@ -21,6 +24,8 @@ where
2124
Provider: StateProviderFactory
2225
+ ChainSpecProvider<ChainSpec = OpChainSpec>
2326
+ BlockReaderIdExt<Header = Header>
27+
+ BlockReader<Block = OpBlock>
28+
+ HeaderProvider<Header = Header>
2429
+ Clone,
2530
{
2631
/// Creates a new instance of MeteringApi
@@ -35,6 +40,8 @@ where
3540
Provider: StateProviderFactory
3641
+ ChainSpecProvider<ChainSpec = OpChainSpec>
3742
+ BlockReaderIdExt<Header = Header>
43+
+ BlockReader<Block = OpBlock>
44+
+ HeaderProvider<Header = Header>
3845
+ Clone
3946
+ Send
4047
+ Sync
@@ -124,4 +131,105 @@ where
124131
total_execution_time_us: total_execution_time,
125132
})
126133
}
134+
135+
async fn meter_block_by_hash(&self, hash: B256) -> RpcResult<MeterBlockResponse> {
136+
info!(block_hash = %hash, "Starting block metering by hash");
137+
138+
let block = self
139+
.provider
140+
.block_by_hash(hash)
141+
.map_err(|e| {
142+
error!(error = %e, "Failed to get block by hash");
143+
jsonrpsee::types::ErrorObjectOwned::owned(
144+
jsonrpsee::types::ErrorCode::InternalError.code(),
145+
format!("Failed to get block: {}", e),
146+
None::<()>,
147+
)
148+
})?
149+
.ok_or_else(|| {
150+
jsonrpsee::types::ErrorObjectOwned::owned(
151+
jsonrpsee::types::ErrorCode::InvalidParams.code(),
152+
format!("Block not found: {}", hash),
153+
None::<()>,
154+
)
155+
})?;
156+
157+
let response = self.meter_block_internal(&block)?;
158+
159+
info!(
160+
block_hash = %hash,
161+
signer_recovery_time_us = response.signer_recovery_time_us,
162+
execution_time_us = response.execution_time_us,
163+
state_root_time_us = response.state_root_time_us,
164+
total_time_us = response.total_time_us,
165+
"Block metering completed successfully"
166+
);
167+
168+
Ok(response)
169+
}
170+
171+
async fn meter_block_by_number(
172+
&self,
173+
number: BlockNumberOrTag,
174+
) -> RpcResult<MeterBlockResponse> {
175+
info!(block_number = ?number, "Starting block metering by number");
176+
177+
let block = self
178+
.provider
179+
.block_by_number_or_tag(number)
180+
.map_err(|e| {
181+
error!(error = %e, "Failed to get block by number");
182+
jsonrpsee::types::ErrorObjectOwned::owned(
183+
jsonrpsee::types::ErrorCode::InternalError.code(),
184+
format!("Failed to get block: {}", e),
185+
None::<()>,
186+
)
187+
})?
188+
.ok_or_else(|| {
189+
jsonrpsee::types::ErrorObjectOwned::owned(
190+
jsonrpsee::types::ErrorCode::InvalidParams.code(),
191+
format!("Block not found: {:?}", number),
192+
None::<()>,
193+
)
194+
})?;
195+
196+
let response = self.meter_block_internal(&block)?;
197+
198+
info!(
199+
block_number = ?number,
200+
block_hash = %response.block_hash,
201+
signer_recovery_time_us = response.signer_recovery_time_us,
202+
execution_time_us = response.execution_time_us,
203+
state_root_time_us = response.state_root_time_us,
204+
total_time_us = response.total_time_us,
205+
"Block metering completed successfully"
206+
);
207+
208+
Ok(response)
209+
}
210+
}
211+
212+
impl<Provider> MeteringApiImpl<Provider>
213+
where
214+
Provider: StateProviderFactory
215+
+ ChainSpecProvider<ChainSpec = OpChainSpec>
216+
+ BlockReaderIdExt<Header = Header>
217+
+ BlockReader<Block = OpBlock>
218+
+ HeaderProvider<Header = Header>
219+
+ Clone
220+
+ Send
221+
+ Sync
222+
+ 'static,
223+
{
224+
/// Internal helper to meter a block's execution
225+
fn meter_block_internal(&self, block: &OpBlock) -> RpcResult<MeterBlockResponse> {
226+
meter_block(self.provider.clone(), self.provider.chain_spec(), block).map_err(|e| {
227+
error!(error = %e, "Block metering failed");
228+
jsonrpsee::types::ErrorObjectOwned::owned(
229+
jsonrpsee::types::ErrorCode::InternalError.code(),
230+
format!("Block metering failed: {}", e),
231+
None::<()>,
232+
)
233+
})
234+
}
127235
}

crates/rpc/src/base/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub(crate) mod block;
12
pub(crate) mod meter;
23
pub(crate) mod meter_rpc;
34
pub(crate) mod pubsub;

crates/rpc/src/base/traits.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,46 @@
11
//! Traits for the RPC module.
22
3-
use alloy_primitives::TxHash;
3+
use alloy_eips::BlockNumberOrTag;
4+
use alloy_primitives::{B256, TxHash};
45
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
56

6-
use crate::{Bundle, MeterBundleResponse, TransactionStatusResponse};
7+
use crate::{Bundle, MeterBlockResponse, MeterBundleResponse, TransactionStatusResponse};
78

89
/// RPC API for transaction metering
910
#[rpc(server, namespace = "base")]
1011
pub trait MeteringApi {
1112
/// Simulates and meters a bundle of transactions
1213
#[method(name = "meterBundle")]
1314
async fn meter_bundle(&self, bundle: Bundle) -> RpcResult<MeterBundleResponse>;
15+
16+
/// Handler for: `base_meterBlockByHash`
17+
///
18+
/// Re-executes a block and returns timing metrics for EVM execution and state root calculation.
19+
///
20+
/// This method fetches the block by hash, re-executes all transactions against the parent
21+
/// block's state, and measures:
22+
/// - `executionTimeUs`: Time to execute all transactions in the EVM
23+
/// - `stateRootTimeUs`: Time to compute the state root after execution
24+
/// - `totalTimeUs`: Sum of execution and state root calculation time
25+
/// - `meteredTransactions`: Per-transaction execution times and gas usage
26+
#[method(name = "meterBlockByHash")]
27+
async fn meter_block_by_hash(&self, hash: B256) -> RpcResult<MeterBlockResponse>;
28+
29+
/// Handler for: `base_meterBlockByNumber`
30+
///
31+
/// Re-executes a block and returns timing metrics for EVM execution and state root calculation.
32+
///
33+
/// This method fetches the block by number, re-executes all transactions against the parent
34+
/// block's state, and measures:
35+
/// - `executionTimeUs`: Time to execute all transactions in the EVM
36+
/// - `stateRootTimeUs`: Time to compute the state root after execution
37+
/// - `totalTimeUs`: Sum of execution and state root calculation time
38+
/// - `meteredTransactions`: Per-transaction execution times and gas usage
39+
#[method(name = "meterBlockByNumber")]
40+
async fn meter_block_by_number(
41+
&self,
42+
number: BlockNumberOrTag,
43+
) -> RpcResult<MeterBlockResponse>;
1444
}
1545

1646
/// RPC API for transaction status

crates/rpc/src/base/types.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Types for the transaction status rpc
22
3+
use alloy_primitives::B256;
34
use alloy_rpc_types_eth::pubsub::SubscriptionKind;
45
use serde::{Deserialize, Serialize};
56

@@ -95,3 +96,41 @@ impl From<BaseSubscriptionKind> for ExtendedSubscriptionKind {
9596
Self::Base(kind)
9697
}
9798
}
99+
100+
// Block metering types
101+
102+
/// Response for block metering RPC calls.
103+
/// Contains the block hash plus timing information for EVM execution and state root calculation.
104+
#[derive(Debug, Clone, Serialize, Deserialize)]
105+
#[serde(rename_all = "camelCase")]
106+
pub struct MeterBlockResponse {
107+
/// The block hash that was metered
108+
pub block_hash: B256,
109+
/// The block number that was metered
110+
pub block_number: u64,
111+
/// Duration of signer recovery in microseconds (can be parallelized)
112+
pub signer_recovery_time_us: u128,
113+
/// Duration of EVM execution in microseconds
114+
pub execution_time_us: u128,
115+
/// Duration of state root calculation in microseconds.
116+
///
117+
/// Note: This timing is most accurate for recent blocks where state tries are cached.
118+
/// For older blocks, trie nodes may not be cached, which can significantly inflate this value.
119+
pub state_root_time_us: u128,
120+
/// Total duration (signer recovery + EVM execution + state root calculation) in microseconds
121+
pub total_time_us: u128,
122+
/// Per-transaction metering data
123+
pub transactions: Vec<MeterBlockTransactions>,
124+
}
125+
126+
/// Metering data for a single transaction
127+
#[derive(Debug, Clone, Serialize, Deserialize)]
128+
#[serde(rename_all = "camelCase")]
129+
pub struct MeterBlockTransactions {
130+
/// Transaction hash
131+
pub tx_hash: B256,
132+
/// Gas used by this transaction
133+
pub gas_used: u64,
134+
/// Execution time in microseconds
135+
pub execution_time_us: u128,
136+
}

0 commit comments

Comments
 (0)