From 8319f2734ffb1cb5006cc1195ca0f9bb063f2859 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Sat, 10 Jan 2026 23:35:58 -0600 Subject: [PATCH] Add state root timing to bundle metering Measure and report time spent calculating state root separately from transaction execution, enabling better performance analysis: - Add MeterBundleOutput struct with state_root_time_us field - Calculate state root after transaction execution using merge_transitions - Track state root calculation time separately from total execution time - Update RPC to log state_root_time_us in metering response - Add meter_bundle_state_root_time_invariant test The state root is computed by calling hashed_post_state on the bundle and then state_root_with_updates on the state provider. --- Cargo.lock | 1 + .../builder/op-rbuilder/src/tests/backrun.rs | 7 + .../builder/op-rbuilder/src/tx_data_store.rs | 2 + crates/client/metering/Cargo.toml | 3 + crates/client/metering/src/lib.rs | 2 +- crates/client/metering/src/meter.rs | 151 +++++++++++++----- crates/client/metering/src/rpc.rs | 30 ++-- crates/shared/bundles/src/meter.rs | 6 + crates/shared/bundles/src/test_utils.rs | 1 + 9 files changed, 151 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7613862..57c4a067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2058,6 +2058,7 @@ dependencies = [ "reth-primitives-traits", "reth-provider", "reth-transaction-pool", + "revm-database", "serde", "tokio", "tracing", diff --git a/crates/builder/op-rbuilder/src/tests/backrun.rs b/crates/builder/op-rbuilder/src/tests/backrun.rs index d5efcd87..a1522a3c 100644 --- a/crates/builder/op-rbuilder/src/tests/backrun.rs +++ b/crates/builder/op-rbuilder/src/tests/backrun.rs @@ -74,6 +74,7 @@ async fn backrun_bundle_all_or_nothing_revert(rbuilder: LocalInstance) -> eyre:: state_flashblock_index: None, total_gas_used: 0, total_execution_time_us: 0, + state_root_time_us: 0, }, }; @@ -193,6 +194,7 @@ async fn backrun_bundles_sorted_by_total_fee(rbuilder: LocalInstance) -> eyre::R state_flashblock_index: None, total_gas_used: 0, total_execution_time_us: 0, + state_root_time_us: 0, }, }; @@ -218,6 +220,7 @@ async fn backrun_bundles_sorted_by_total_fee(rbuilder: LocalInstance) -> eyre::R state_flashblock_index: None, total_gas_used: 0, total_execution_time_us: 0, + state_root_time_us: 0, }, }; @@ -358,6 +361,7 @@ async fn backrun_bundle_rejected_low_total_fee(rbuilder: LocalInstance) -> eyre: state_flashblock_index: None, total_gas_used: 0, total_execution_time_us: 0, + state_root_time_us: 0, }, }; @@ -451,6 +455,7 @@ async fn backrun_bundle_rejected_exceeds_gas_limit(rbuilder: LocalInstance) -> e state_flashblock_index: None, total_gas_used: 0, total_execution_time_us: 0, + state_root_time_us: 0, }, }; @@ -535,6 +540,7 @@ async fn backrun_bundle_rejected_exceeds_da_limit(rbuilder: LocalInstance) -> ey state_flashblock_index: None, total_gas_used: 0, total_execution_time_us: 0, + state_root_time_us: 0, }, }; @@ -621,6 +627,7 @@ async fn backrun_bundle_invalid_tx_skipped(rbuilder: LocalInstance) -> eyre::Res state_flashblock_index: None, total_gas_used: 0, total_execution_time_us: 0, + state_root_time_us: 0, }, }; diff --git a/crates/builder/op-rbuilder/src/tx_data_store.rs b/crates/builder/op-rbuilder/src/tx_data_store.rs index affa1b8d..05dd1d10 100644 --- a/crates/builder/op-rbuilder/src/tx_data_store.rs +++ b/crates/builder/op-rbuilder/src/tx_data_store.rs @@ -386,6 +386,7 @@ mod tests { state_flashblock_index: None, total_gas_used: 0, total_execution_time_us: 0, + state_root_time_us: 0, }, } } @@ -402,6 +403,7 @@ mod tests { state_flashblock_index: None, total_gas_used: gas_used, total_execution_time_us: 533, + state_root_time_us: 0, } } diff --git a/crates/client/metering/Cargo.toml b/crates/client/metering/Cargo.toml index c45ee302..94588f01 100644 --- a/crates/client/metering/Cargo.toml +++ b/crates/client/metering/Cargo.toml @@ -25,6 +25,9 @@ reth-optimism-evm.workspace = true reth-optimism-chainspec.workspace = true reth-optimism-primitives.workspace = true +# revm +revm-database.workspace = true + # alloy alloy-primitives.workspace = true alloy-consensus.workspace = true diff --git a/crates/client/metering/src/lib.rs b/crates/client/metering/src/lib.rs index 41883a8d..7f90aedb 100644 --- a/crates/client/metering/src/lib.rs +++ b/crates/client/metering/src/lib.rs @@ -10,7 +10,7 @@ mod extension; pub use extension::MeteringExtension; mod meter; -pub use meter::meter_bundle; +pub use meter::{MeterBundleOutput, meter_bundle}; mod rpc; pub use rpc::MeteringApiImpl; diff --git a/crates/client/metering/src/meter.rs b/crates/client/metering/src/meter.rs index 15bd0cdf..82c2475e 100644 --- a/crates/client/metering/src/meter.rs +++ b/crates/client/metering/src/meter.rs @@ -11,26 +11,40 @@ use reth_evm::{ConfigureEvm, execute::BlockBuilder}; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; use reth_primitives_traits::SealedHeader; +use revm_database::states::bundle_state::BundleRetention; const BLOCK_TIME: u64 = 2; // 2 seconds per block +/// Output from metering a bundle of transactions +#[derive(Debug)] +pub struct MeterBundleOutput { + /// Transaction results with individual metrics + pub results: Vec, + /// Total gas used by all transactions + pub total_gas_used: u64, + /// Total gas fees paid by all transactions + pub total_gas_fees: U256, + /// Bundle hash + pub bundle_hash: B256, + /// Total time in microseconds (includes transaction execution and state root calculation) + pub total_time_us: u128, + /// State root calculation time in microseconds + pub state_root_time_us: u128, +} + /// Simulates and meters a bundle of transactions /// -/// Takes a state provider, chain spec, decoded transactions, block header, and bundle metadata, -/// and executes transactions in sequence to measure gas usage and execution time. +/// Takes a state provider, chain spec, parsed bundle, and block header, +/// then executes transactions in sequence to measure gas usage and execution time. /// -/// Returns a tuple of: -/// - Vector of transaction results -/// - Total gas used -/// - Total gas fees paid -/// - Bundle hash -/// - Total execution time in microseconds +/// Returns [`MeterBundleOutput`] containing transaction results and aggregated metrics, +/// including separate timing for state root calculation. pub fn meter_bundle( state_provider: SP, chain_spec: Arc, bundle: ParsedBundle, header: &SealedHeader, -) -> EyreResult<(Vec, u64, U256, B256, u128)> +) -> EyreResult where SP: reth_provider::StateProvider, { @@ -58,7 +72,7 @@ where let mut total_gas_used = 0u64; let mut total_gas_fees = U256::ZERO; - let execution_start = Instant::now(); + let total_start = Instant::now(); { let evm_config = OpEvmConfig::optimism(chain_spec); let mut builder = evm_config.builder_for_next_block(&mut db, header, attributes)?; @@ -85,7 +99,7 @@ where results.push(TransactionResult { coinbase_diff: gas_fees, - eth_sent_to_coinbase: U256::from(0), + eth_sent_to_coinbase: U256::ZERO, from_address: from, gas_fees, gas_price: U256::from(gas_price), @@ -97,9 +111,26 @@ where }); } } - let total_execution_time = execution_start.elapsed().as_micros(); - Ok((results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time)) + // Calculate state root and measure its calculation time + db.merge_transitions(BundleRetention::Reverts); + let bundle_update = db.take_bundle(); + let state_provider = db.database.as_ref(); + + let state_root_start = Instant::now(); + let hashed_state = state_provider.hashed_post_state(&bundle_update); + let _ = state_provider.state_root_with_updates(hashed_state)?; + let state_root_time_us = state_root_start.elapsed().as_micros(); + let total_time_us = total_start.elapsed().as_micros(); + + Ok(MeterBundleOutput { + results, + total_gas_used, + total_gas_fees, + bundle_hash, + total_time_us, + state_root_time_us, + }) } #[cfg(test)] @@ -146,15 +177,15 @@ mod tests { let parsed_bundle = create_parsed_bundle(Vec::new())?; - let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = - meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header)?; + let output = meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header)?; - assert!(results.is_empty()); - assert_eq!(total_gas_used, 0); - assert_eq!(total_gas_fees, U256::ZERO); + assert!(output.results.is_empty()); + assert_eq!(output.total_gas_used, 0); + assert_eq!(output.total_gas_fees, U256::ZERO); // Even empty bundles have some EVM setup overhead - assert!(total_execution_time > 0); - assert_eq!(bundle_hash, keccak256([])); + assert!(output.total_time_us > 0); + assert!(output.state_root_time_us > 0); + assert_eq!(output.bundle_hash, keccak256([])); Ok(()) } @@ -189,12 +220,12 @@ mod tests { let parsed_bundle = create_parsed_bundle(vec![tx])?; - let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = - meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header)?; + let output = meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header)?; - assert_eq!(results.len(), 1); - let result = &results[0]; - assert!(total_execution_time > 0); + assert_eq!(output.results.len(), 1); + let result = &output.results[0]; + assert!(output.total_time_us > 0); + assert!(output.state_root_time_us > 0); assert_eq!(result.from_address, Account::Alice.address()); assert_eq!(result.to_address, Some(to)); @@ -203,12 +234,12 @@ mod tests { assert_eq!(result.gas_used, 21_000); assert_eq!(result.coinbase_diff, (U256::from(21_000) * U256::from(10)),); - assert_eq!(total_gas_used, 21_000); - assert_eq!(total_gas_fees, U256::from(21_000) * U256::from(10)); + assert_eq!(output.total_gas_used, 21_000); + assert_eq!(output.total_gas_fees, U256::from(21_000) * U256::from(10)); let mut concatenated = Vec::with_capacity(32); concatenated.extend_from_slice(tx_hash.as_slice()); - assert_eq!(bundle_hash, keccak256(concatenated)); + assert_eq!(output.bundle_hash, keccak256(concatenated)); assert!(result.execution_time_us > 0, "execution_time_us should be greater than zero"); @@ -266,14 +297,14 @@ mod tests { let parsed_bundle = create_parsed_bundle(vec![tx_1, tx_2])?; - let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = - meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header)?; + let output = meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header)?; - assert_eq!(results.len(), 2); - assert!(total_execution_time > 0); + assert_eq!(output.results.len(), 2); + assert!(output.total_time_us > 0); + assert!(output.state_root_time_us > 0); // Check first transaction - let result_1 = &results[0]; + let result_1 = &output.results[0]; assert_eq!(result_1.from_address, Account::Alice.address()); assert_eq!(result_1.to_address, Some(to_1)); assert_eq!(result_1.tx_hash, tx_hash_1); @@ -282,7 +313,7 @@ mod tests { assert_eq!(result_1.coinbase_diff, (U256::from(21_000) * U256::from(10)),); // Check second transaction - let result_2 = &results[1]; + let result_2 = &output.results[1]; assert_eq!(result_2.from_address, Account::Bob.address()); assert_eq!(result_2.to_address, Some(to_2)); assert_eq!(result_2.tx_hash, tx_hash_2); @@ -291,20 +322,66 @@ mod tests { assert_eq!(result_2.coinbase_diff, U256::from(21_000) * U256::from(15),); // Check aggregated values - assert_eq!(total_gas_used, 42_000); + assert_eq!(output.total_gas_used, 42_000); let expected_total_fees = U256::from(21_000) * U256::from(10) + U256::from(21_000) * U256::from(15); - assert_eq!(total_gas_fees, expected_total_fees); + assert_eq!(output.total_gas_fees, expected_total_fees); // Check bundle hash includes both transactions let mut concatenated = Vec::with_capacity(64); concatenated.extend_from_slice(tx_hash_1.as_slice()); concatenated.extend_from_slice(tx_hash_2.as_slice()); - assert_eq!(bundle_hash, keccak256(concatenated)); + assert_eq!(output.bundle_hash, keccak256(concatenated)); assert!(result_1.execution_time_us > 0, "execution_time_us should be greater than zero"); assert!(result_2.execution_time_us > 0, "execution_time_us should be greater than zero"); Ok(()) } + + /// Test that state_root_time_us is always <= total_time_us + #[tokio::test] + async fn meter_bundle_state_root_time_invariant() -> eyre::Result<()> { + let harness = TestHarness::new().await?; + let latest = harness.latest_block(); + let header = latest.sealed_header().clone(); + + let to = Address::random(); + let signed_tx = TransactionBuilder::default() + .signer(Account::Alice.signer_b256()) + .chain_id(harness.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 state_provider = harness + .blockchain_provider() + .state_by_block_hash(latest.hash()) + .context("getting state provider")?; + + let parsed_bundle = create_parsed_bundle(vec![tx])?; + + let output = meter_bundle(state_provider, harness.chain_spec(), parsed_bundle, &header)?; + + // Verify invariant: total time must include state root time + assert!( + output.total_time_us >= output.state_root_time_us, + "total_time_us ({}) should be >= state_root_time_us ({})", + output.total_time_us, + output.state_root_time_us + ); + + // State root time should be non-zero + assert!(output.state_root_time_us > 0, "state_root_time_us should be greater than zero"); + + Ok(()) + } } diff --git a/crates/client/metering/src/rpc.rs b/crates/client/metering/src/rpc.rs index d74649d2..0da72d23 100644 --- a/crates/client/metering/src/rpc.rs +++ b/crates/client/metering/src/rpc.rs @@ -94,7 +94,7 @@ where })?; // Meter bundle using utility function - let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = + let output = meter_bundle(state_provider, self.provider.chain_spec(), parsed_bundle, &header) .map_err(|e| { error!(error = %e, "Bundle metering failed"); @@ -106,31 +106,33 @@ where })?; // Calculate average gas price - let bundle_gas_price = if total_gas_used > 0 { - total_gas_fees / U256::from(total_gas_used) + let bundle_gas_price = if output.total_gas_used > 0 { + output.total_gas_fees / U256::from(output.total_gas_used) } else { U256::from(0) }; info!( - bundle_hash = %bundle_hash, - num_transactions = results.len(), - total_gas_used = total_gas_used, - total_execution_time_us = total_execution_time, + bundle_hash = %output.bundle_hash, + num_transactions = output.results.len(), + total_gas_used = output.total_gas_used, + total_time_us = output.total_time_us, + state_root_time_us = output.state_root_time_us, "Bundle metering completed successfully" ); Ok(MeterBundleResponse { bundle_gas_price, - bundle_hash, - coinbase_diff: total_gas_fees, - eth_sent_to_coinbase: U256::from(0), - gas_fees: total_gas_fees, - results, + bundle_hash: output.bundle_hash, + coinbase_diff: output.total_gas_fees, + eth_sent_to_coinbase: U256::ZERO, + gas_fees: output.total_gas_fees, + results: output.results, state_block_number: header.number, state_flashblock_index: None, - total_gas_used, - total_execution_time_us: total_execution_time, + total_gas_used: output.total_gas_used, + total_execution_time_us: output.total_time_us, + state_root_time_us: output.state_root_time_us, }) } diff --git a/crates/shared/bundles/src/meter.rs b/crates/shared/bundles/src/meter.rs index f237eea2..0b34a7dd 100644 --- a/crates/shared/bundles/src/meter.rs +++ b/crates/shared/bundles/src/meter.rs @@ -58,6 +58,9 @@ pub struct MeterBundleResponse { pub total_gas_used: u64, /// Total execution time in microseconds. pub total_execution_time_us: u128, + /// Time spent calculating state root in microseconds. + #[serde(default)] + pub state_root_time_us: u128, } #[cfg(test)] @@ -136,11 +139,13 @@ mod tests { state_flashblock_index: Some(42), total_gas_used: 21000, total_execution_time_us: 1000, + state_root_time_us: 500, }; let json = serde_json::to_string(&response).unwrap(); assert!(json.contains("\"stateFlashblockIndex\":42")); assert!(json.contains("\"stateBlockNumber\":12345")); + assert!(json.contains("\"stateRootTimeUs\":500")); let deserialized: MeterBundleResponse = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.state_flashblock_index, Some(42)); @@ -160,6 +165,7 @@ mod tests { state_flashblock_index: None, total_gas_used: 21000, total_execution_time_us: 1000, + state_root_time_us: 0, }; let json = serde_json::to_string(&response).unwrap(); diff --git a/crates/shared/bundles/src/test_utils.rs b/crates/shared/bundles/src/test_utils.rs index 1d93b529..5ba945f0 100644 --- a/crates/shared/bundles/src/test_utils.rs +++ b/crates/shared/bundles/src/test_utils.rs @@ -84,6 +84,7 @@ pub fn create_test_meter_bundle_response() -> MeterBundleResponse { state_flashblock_index: None, total_gas_used: 0, total_execution_time_us: 0, + state_root_time_us: 0, } }