diff --git a/Cargo.lock b/Cargo.lock index fdaac57..f53775f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,7 +243,7 @@ dependencies = [ "alloy-sol-types", "auto_impl", "derive_more", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-rpc-types-engine", "op-revm", "revm", @@ -356,7 +356,7 @@ dependencies = [ "alloy-op-hardforks", "alloy-primitives", "auto_impl", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-revm", "revm", ] @@ -1518,7 +1518,7 @@ dependencies = [ "metrics", "metrics-derive", "once_cell", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-network", "op-alloy-rpc-types", "rand 0.9.2", @@ -1541,6 +1541,7 @@ dependencies = [ "reth-rpc-eth-api", "reth-testing-utils", "reth-tracing", + "revm-database", "rollup-boost", "serde", "serde_json", @@ -1560,9 +1561,11 @@ dependencies = [ "alloy-genesis", "alloy-primitives", "alloy-rpc-client", + "arc-swap", + "base-reth-flashblocks-rpc", "eyre", "jsonrpsee 0.26.0", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "rand 0.9.2", "reth", "reth-db", @@ -1580,7 +1583,9 @@ dependencies = [ "reth-testing-utils", "reth-tracing", "reth-transaction-pool 1.8.2", + "reth-trie-common 1.8.2", "revm", + "revm-database", "serde", "serde_json", "tips-core", @@ -1614,7 +1619,7 @@ dependencies = [ "metrics", "metrics-derive", "once_cell", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-network", "op-alloy-rpc-jsonrpsee", "op-alloy-rpc-types", @@ -1669,7 +1674,7 @@ dependencies = [ "futures-util", "jsonrpsee 0.26.0", "once_cell", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-network", "op-alloy-rpc-types", "op-alloy-rpc-types-engine", @@ -5964,20 +5969,6 @@ dependencies = [ "thiserror 2.0.17", ] -[[package]] -name = "op-alloy-consensus" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1fc8aa0e2f5b136d101630be009e4e6dbdd1f17bc3ce670f431511600d2930" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "derive_more", - "thiserror 2.0.17", -] - [[package]] name = "op-alloy-flz" version = "0.13.1" @@ -5996,7 +5987,7 @@ dependencies = [ "alloy-provider", "alloy-rpc-types-eth", "alloy-signer", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-rpc-types", ] @@ -6023,7 +6014,7 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-serde", "derive_more", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "serde", "serde_json", "thiserror 2.0.17", @@ -6044,7 +6035,7 @@ dependencies = [ "derive_more", "ethereum_ssz", "ethereum_ssz_derive", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "serde", "snap", "thiserror 2.0.17", @@ -7455,7 +7446,7 @@ dependencies = [ "alloy-trie", "bytes", "modular-bitfield", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "reth-codecs-derive 1.8.1", "reth-zstd-compressors 1.8.1", "serde", @@ -7474,7 +7465,7 @@ dependencies = [ "arbitrary", "bytes", "modular-bitfield", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "reth-codecs-derive 1.8.2", "reth-zstd-compressors 1.8.2", "serde", @@ -9146,7 +9137,7 @@ dependencies = [ "alloy-hardforks", "alloy-primitives", "derive_more", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-rpc-types", "reth-chainspec 1.8.1", "reth-ethereum-forks 1.8.1", @@ -9170,7 +9161,7 @@ dependencies = [ "alloy-primitives", "derive_more", "miniz_oxide", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-rpc-types", "paste", "reth-chainspec 1.8.2", @@ -9198,7 +9189,7 @@ dependencies = [ "derive_more", "eyre", "futures-util", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "reth-chainspec 1.8.2", "reth-cli", "reth-cli-commands", @@ -9293,7 +9284,7 @@ dependencies = [ "alloy-evm", "alloy-op-evm", "alloy-primitives", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-rpc-types-engine", "op-revm", "reth-chainspec 1.8.1", @@ -9320,7 +9311,7 @@ dependencies = [ "alloy-evm", "alloy-op-evm", "alloy-primitives", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-rpc-types-engine", "op-revm", "reth-chainspec 1.8.2", @@ -9406,7 +9397,7 @@ dependencies = [ "alloy-rpc-types-eth", "clap", "eyre", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-rpc-types-engine", "op-revm", "reth-chainspec 1.8.2", @@ -9453,7 +9444,7 @@ dependencies = [ "alloy-rpc-types-debug", "alloy-rpc-types-engine", "derive_more", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-rpc-types-engine", "reth-basic-payload-builder 1.8.1", "reth-chain-state 1.8.1", @@ -9492,7 +9483,7 @@ dependencies = [ "alloy-rpc-types-debug", "alloy-rpc-types-engine", "derive_more", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-rpc-types-engine", "reth-basic-payload-builder 1.8.2", "reth-chain-state 1.8.2", @@ -9528,7 +9519,7 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "reth-primitives-traits 1.8.1", ] @@ -9544,7 +9535,7 @@ dependencies = [ "arbitrary", "bytes", "modular-bitfield", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "reth-codecs 1.8.2", "reth-primitives-traits 1.8.2", "reth-zstd-compressors 1.8.2", @@ -9575,7 +9566,7 @@ dependencies = [ "jsonrpsee-core 0.26.0", "jsonrpsee-types 0.26.0", "metrics", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-network", "op-alloy-rpc-jsonrpsee", "op-alloy-rpc-types", @@ -9640,7 +9631,7 @@ dependencies = [ "derive_more", "futures-util", "metrics", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-flz", "op-alloy-rpc-types", "op-revm", @@ -9676,7 +9667,7 @@ dependencies = [ "derive_more", "futures-util", "metrics", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-flz", "op-alloy-rpc-types", "op-revm", @@ -9872,7 +9863,7 @@ dependencies = [ "bytes", "derive_more", "once_cell", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "reth-codecs 1.8.1", "revm-bytecode", "revm-primitives", @@ -9902,7 +9893,7 @@ dependencies = [ "derive_more", "modular-bitfield", "once_cell", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "proptest", "proptest-arbitrary-interop", "rayon", @@ -10245,7 +10236,7 @@ dependencies = [ "auto_impl", "dyn-clone", "jsonrpsee-types 0.26.0", - "op-alloy-consensus 0.20.0", + "op-alloy-consensus", "op-alloy-network", "op-alloy-rpc-types", "op-revm", @@ -12555,13 +12546,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tips-core" version = "0.1.0" -source = "git+https://github.com/base/tips?rev=27674ae051a86033ece61ae24434aeacdb28ce73#27674ae051a86033ece61ae24434aeacdb28ce73" +source = "git+https://github.com/base/tips?rev=86b275c0fd63226c3fb85ac5512033f99b67d0f5#86b275c0fd63226c3fb85ac5512033f99b67d0f5" dependencies = [ "alloy-consensus", "alloy-primitives", "alloy-provider", "alloy-serde", - "op-alloy-consensus 0.21.0", + "op-alloy-consensus", + "op-alloy-flz", "serde", "tracing", "tracing-subscriber 0.3.20", @@ -13529,7 +13521,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5b0aad0..99c352a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ base-reth-transaction-tracing = { path = "crates/transaction-tracing" } # base/tips # Note: default-features = false avoids version conflicts with reth's alloy/op-alloy dependencies -tips-core = { git = "https://github.com/base/tips", rev = "27674ae051a86033ece61ae24434aeacdb28ce73", default-features = false } +tips-core = { git = "https://github.com/base/tips", rev = "86b275c0fd63226c3fb85ac5512033f99b67d0f5", default-features = false } # reth reth = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } @@ -75,10 +75,12 @@ reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-testing-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-db-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-ipc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } +reth-trie-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } # revm revm = { version = "29.0.0", default-features = false } revm-bytecode = { version = "6.2.2", default-features = false } +revm-database = { version = "7.0.5", default-features = false } # alloy alloy-primitives = { version = "1.3.1", default-features = false, features = [ diff --git a/crates/flashblocks-rpc/Cargo.toml b/crates/flashblocks-rpc/Cargo.toml index cb54382..5faa9b2 100644 --- a/crates/flashblocks-rpc/Cargo.toml +++ b/crates/flashblocks-rpc/Cargo.toml @@ -29,6 +29,9 @@ reth-primitives.workspace = true reth-primitives-traits.workspace = true reth-exex.workspace = true +# revm +revm-database.workspace = true + # alloy alloy-primitives.workspace = true alloy-eips.workspace = true diff --git a/crates/flashblocks-rpc/src/pending_blocks.rs b/crates/flashblocks-rpc/src/pending_blocks.rs index e93229a..c4d9d5c 100644 --- a/crates/flashblocks-rpc/src/pending_blocks.rs +++ b/crates/flashblocks-rpc/src/pending_blocks.rs @@ -10,7 +10,10 @@ use alloy_rpc_types_eth::{Filter, Header as RPCHeader, Log}; use eyre::eyre; use op_alloy_network::Optimism; use op_alloy_rpc_types::{OpTransactionReceipt, Transaction}; -use reth::revm::{db::Cache, state::EvmState}; +use reth::revm::{ + db::{BundleState, Cache}, + state::EvmState, +}; use reth_rpc_eth_api::RpcBlock; use crate::subscription::Flashblock; @@ -28,6 +31,7 @@ pub struct PendingBlocksBuilder { state_overrides: Option, db_cache: Cache, + bundle_state: BundleState, } impl PendingBlocksBuilder { @@ -43,6 +47,7 @@ impl PendingBlocksBuilder { transaction_state: HashMap::new(), state_overrides: None, db_cache: Cache::default(), + bundle_state: BundleState::default(), } } @@ -107,6 +112,12 @@ impl PendingBlocksBuilder { self } + #[inline] + pub(crate) fn with_bundle_state(&mut self, bundle_state: BundleState) -> &Self { + self.bundle_state = bundle_state; + self + } + pub(crate) fn build(self) -> eyre::Result { if self.headers.is_empty() { return Err(eyre!("missing headers")); @@ -127,6 +138,7 @@ impl PendingBlocksBuilder { transaction_state: self.transaction_state, state_overrides: self.state_overrides, db_cache: self.db_cache, + bundle_state: self.bundle_state, }) } } @@ -145,6 +157,7 @@ pub struct PendingBlocks { state_overrides: Option, db_cache: Cache, + bundle_state: BundleState, } impl PendingBlocks { @@ -176,6 +189,10 @@ impl PendingBlocks { self.db_cache.clone() } + pub fn get_bundle_state(&self) -> BundleState { + self.bundle_state.clone() + } + pub fn get_transactions_for_block(&self, block_number: BlockNumber) -> Vec { self.transactions .iter() diff --git a/crates/flashblocks-rpc/src/state.rs b/crates/flashblocks-rpc/src/state.rs index fd060cc..3a17dec 100644 --- a/crates/flashblocks-rpc/src/state.rs +++ b/crates/flashblocks-rpc/src/state.rs @@ -30,6 +30,7 @@ use reth_optimism_rpc::OpReceiptBuilder; use reth_primitives::RecoveredBlock; use reth_rpc_convert::{transaction::ConvertReceiptInput, RpcTransaction}; use reth_rpc_eth_api::{RpcBlock, RpcReceipt}; +use revm_database::states::bundle_state::BundleRetention; use std::collections::{BTreeMap, HashSet}; use std::sync::Arc; use std::time::Instant; @@ -405,18 +406,30 @@ where .client .state_by_block_number_or_tag(BlockNumberOrTag::Number(canonical_block))?; let state_provider_db = StateProviderDatabase::new(state_provider); - let state = State::builder() - .with_database(state_provider_db) - .with_bundle_update() - .build(); let mut pending_blocks_builder = PendingBlocksBuilder::new(); - let mut db = match &prev_pending_blocks { + // Cache reads across flashblocks, accumulating caches from previous + // pending blocks if available + let cache_db = match &prev_pending_blocks { Some(pending_blocks) => CacheDB { cache: pending_blocks.get_db_cache(), - db: state, + db: state_provider_db, }, - None => CacheDB::new(state), + None => CacheDB::new(state_provider_db), + }; + + // Track state changes across flashblocks, accumulating bundle state + // from previous pending blocks if available + let mut db = match &prev_pending_blocks { + Some(pending_blocks) => State::builder() + .with_database(cache_db) + .with_bundle_update() + .with_bundle_prestate(pending_blocks.get_bundle_state()) + .build(), + None => State::builder() + .with_database(cache_db) + .with_bundle_update() + .build(), }; let mut state_overrides = match &prev_pending_blocks { Some(pending_blocks) => pending_blocks.get_state_overrides().unwrap_or_default(), @@ -662,7 +675,9 @@ where last_block_header = block.header.clone(); } - pending_blocks_builder.with_db_cache(db.cache); + db.merge_transitions(BundleRetention::Reverts); + pending_blocks_builder.with_bundle_state(db.take_bundle()); + pending_blocks_builder.with_db_cache(db.database.cache); pending_blocks_builder.with_state_overrides(state_overrides); Ok(Some(Arc::new(pending_blocks_builder.build()?))) } diff --git a/crates/metering/Cargo.toml b/crates/metering/Cargo.toml index 42341f6..04e7662 100644 --- a/crates/metering/Cargo.toml +++ b/crates/metering/Cargo.toml @@ -26,6 +26,7 @@ reth-optimism-chainspec.workspace = true reth-optimism-primitives.workspace = true reth-transaction-pool.workspace = true reth-optimism-cli.workspace = true # Enables serde & codec traits for OpReceipt/OpTxEnvelope +reth-trie-common.workspace = true # alloy alloy-primitives.workspace = true @@ -35,8 +36,12 @@ alloy-eips.workspace = true # op-alloy op-alloy-consensus.workspace = true +# base +base-reth-flashblocks-rpc = { path = "../flashblocks-rpc" } + # revm revm.workspace = true +revm-database.workspace = true # rpc jsonrpsee.workspace = true @@ -45,6 +50,7 @@ jsonrpsee.workspace = true tracing.workspace = true serde.workspace = true eyre.workspace = true +arc-swap.workspace = true [dev-dependencies] alloy-genesis.workspace = true diff --git a/crates/metering/README.md b/crates/metering/README.md index 1cc8834..eefc12f 100644 --- a/crates/metering/README.md +++ b/crates/metering/README.md @@ -11,7 +11,7 @@ Simulates a bundle of transactions, providing gas usage and execution time metri The method accepts a Bundle object with the following fields: - `txs`: Array of signed, RLP-encoded transactions (hex strings with 0x prefix) -- `block_number`: Target block number for bundle validity (note: simulation always uses the latest available block state) +- `block_number`: Target block number for bundle validity (note: simulation uses pending flashblocks state when available, otherwise latest canonical block) - `min_timestamp` (optional): Minimum timestamp for bundle validity (also used as simulation timestamp if provided) - `max_timestamp` (optional): Maximum timestamp for bundle validity - `reverting_tx_hashes` (optional): Array of transaction hashes allowed to revert @@ -26,7 +26,7 @@ The method accepts a Bundle object with the following fields: - `coinbaseDiff`: Total gas fees paid - `ethSentToCoinbase`: ETH sent directly to coinbase - `gasFees`: Total gas fees -- `stateBlockNumber`: Block number used for state (always the latest available block) +- `stateBlockNumber`: Block number used for state (latest flashblock if pending flashblocks exist, otherwise latest canonical block) - `totalGasUsed`: Total gas consumed - `totalExecutionTimeUs`: Total execution time (μs) - `results`: Array of per-transaction results: diff --git a/crates/metering/src/flashblock_trie_cache.rs b/crates/metering/src/flashblock_trie_cache.rs new file mode 100644 index 0000000..d4ac90d --- /dev/null +++ b/crates/metering/src/flashblock_trie_cache.rs @@ -0,0 +1,123 @@ +use alloy_primitives::B256; +use arc_swap::ArcSwap; +use eyre::Result as EyreResult; +use reth_provider::StateProvider; +use std::sync::Arc; + +use crate::FlashblocksState; + +/// Trie nodes and hashed state from computing a flashblock state root. +/// +/// These cached nodes can be reused when computing a bundle's state root +/// to avoid recalculating the flashblock portion of the trie. +#[derive(Debug, Clone)] +pub struct FlashblockTrieData { + pub trie_updates: reth_trie_common::updates::TrieUpdates, + pub hashed_state: reth_trie_common::HashedPostState, +} + +/// Internal cache entry for a single flashblock. +#[derive(Debug, Clone)] +struct CachedFlashblockTrie { + block_hash: B256, + flashblock_index: u64, + /// The cached trie data + trie_data: FlashblockTrieData, +} + +/// Thread-safe single-entry cache for the latest flashblock's trie nodes. +/// +/// This cache stores the intermediate trie nodes computed when calculating +/// the latest flashblock's state root. Subsequent bundle metering operations +/// on the same flashblock can reuse these cached nodes instead of recalculating +/// them, significantly improving performance. +/// +/// **Important**: This cache holds only ONE flashblock's trie at a time. +/// When a new flashblock is cached, it replaces any previously cached flashblock. +/// This design assumes that bundle metering operations are performed on the +/// current/latest flashblock, not historical ones. +#[derive(Debug, Clone)] +pub struct FlashblockTrieCache { + /// Single-entry cache for the latest flashblock's trie + cache: Arc>>, +} + +impl FlashblockTrieCache { + /// Creates a new empty flashblock trie cache. + pub fn new() -> Self { + Self { + cache: Arc::new(ArcSwap::from_pointee(None)), + } + } + + /// Ensures the trie for the given flashblock is cached and returns it. + /// + /// If the cache already contains an entry for the given block_hash and flashblock_index, + /// this returns the cached data immediately without recomputation. Otherwise, it computes + /// the flashblock's state root, caches the resulting trie nodes, **replacing any previously + /// cached flashblock**, and returns the new data. + /// + /// # Single-Entry Cache Behavior + /// + /// This cache only stores one flashblock's trie at a time. Calling this method with a different + /// flashblock will evict the previous entry. This is by design, as the cache is intended for + /// the latest/current flashblock only. + /// + /// # Arguments + /// + /// * `block_hash` - Hash of the block containing the flashblock + /// * `flashblock_index` - Index of the flashblock within the block + /// * `flashblocks_state` - The accumulated state from pending flashblocks + /// * `canonical_state_provider` - State provider for the canonical chain + /// + /// # Returns + /// + /// The cached `FlashblockTrieData` containing the trie updates and hashed state. + pub fn ensure_cached( + &self, + block_hash: B256, + flashblock_index: u64, + flashblocks_state: &FlashblocksState, + canonical_state_provider: &dyn StateProvider, + ) -> EyreResult { + // Check if we already have a cached trie for this exact flashblock + let cached = self.cache.load(); + if let Some(ref cache) = **cached { + if cache.block_hash == block_hash && cache.flashblock_index == flashblock_index { + // Cache is still valid for this flashblock, return it + return Ok(cache.trie_data.clone()); + } + } + + // Need to compute the flashblock trie (this will replace any existing cache entry) + + // Compute hashed post state from the bundle + let hashed_state = + canonical_state_provider.hashed_post_state(&flashblocks_state.bundle_state); + + // Calculate state root with updates to get the trie nodes + let (_state_root, trie_updates) = + canonical_state_provider.state_root_with_updates(hashed_state.clone())?; + + // Create the trie data + let trie_data = FlashblockTrieData { + trie_updates, + hashed_state, + }; + + // Store the new entry, replacing any previous flashblock's cached trie + self.cache.store(Arc::new(Some(CachedFlashblockTrie { + block_hash, + flashblock_index, + trie_data: trie_data.clone(), + }))); + + Ok(trie_data) + } +} + +impl Default for FlashblockTrieCache { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/metering/src/lib.rs b/crates/metering/src/lib.rs index 138a3c7..2c687a2 100644 --- a/crates/metering/src/lib.rs +++ b/crates/metering/src/lib.rs @@ -1,8 +1,10 @@ +mod flashblock_trie_cache; mod meter; mod rpc; #[cfg(test)] mod tests; -pub use meter::meter_bundle; +pub use flashblock_trie_cache::{FlashblockTrieCache, FlashblockTrieData}; +pub use meter::{meter_bundle, FlashblocksState, MeterBundleOutput}; pub use rpc::{MeteringApiImpl, MeteringApiServer}; pub use tips_core::types::{Bundle, MeterBundleResponse, TransactionResult}; diff --git a/crates/metering/src/meter.rs b/crates/metering/src/meter.rs index b43e63f..4658424 100644 --- a/crates/metering/src/meter.rs +++ b/crates/metering/src/meter.rs @@ -1,49 +1,109 @@ use alloy_consensus::{transaction::SignerRecoverable, BlockHeader, Transaction as _}; use alloy_primitives::{B256, U256}; use eyre::{eyre, Result as EyreResult}; -use reth::revm::db::State; +use reth::revm::db::{BundleState, Cache, CacheDB, State}; use reth_evm::execute::BlockBuilder; use reth_evm::ConfigureEvm; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_evm::{OpEvmConfig, OpNextBlockEnvAttributes}; use reth_primitives_traits::SealedHeader; +use reth_trie_common::TrieInput; +use revm_database::states::bundle_state::BundleRetention; use std::sync::Arc; use std::time::Instant; use crate::TransactionResult; +/// State from pending flashblocks that is used as a base for metering +#[derive(Debug, Clone)] +pub struct FlashblocksState { + /// The cache of account and storage data + pub cache: Cache, + /// The accumulated bundle of state changes + pub bundle_state: BundleState, +} + 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 execution time in microseconds (includes state root calculation) + pub total_execution_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. /// -/// 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. pub fn meter_bundle( state_provider: SP, chain_spec: Arc, decoded_txs: Vec, header: &SealedHeader, bundle_with_metadata: &tips_core::types::BundleWithMetadata, -) -> EyreResult<(Vec, u64, U256, B256, u128)> + flashblocks_state: Option, + cached_flashblock_trie: Option, +) -> EyreResult where SP: reth_provider::StateProvider, { // Get bundle hash from BundleWithMetadata let bundle_hash = bundle_with_metadata.bundle_hash(); + // If we have flashblocks but no cached trie, compute the flashblock trie first + // (before starting any timers, since we only want to time the bundle's execution and state root) + let flashblock_trie = if cached_flashblock_trie.is_none() { + if let Some(ref fb_state) = flashblocks_state { + let fb_hashed_state = state_provider.hashed_post_state(&fb_state.bundle_state); + let (_fb_state_root, fb_trie_updates) = + state_provider.state_root_with_updates(fb_hashed_state.clone())?; + Some((fb_trie_updates, fb_hashed_state)) + } else { + None + } + } else { + None + }; + // Create state database let state_db = reth::revm::database::StateProviderDatabase::new(state_provider); - let mut db = State::builder() - .with_database(state_db) - .with_bundle_update() - .build(); + + // If we have flashblocks state, apply both cache and bundle prestate + let cache_db = if let Some(ref flashblocks) = flashblocks_state { + CacheDB { + cache: flashblocks.cache.clone(), + db: state_db, + } + } else { + CacheDB::new(state_db) + }; + + // Wrap the CacheDB in a State to track bundle changes for state root calculation + let mut db = if let Some(flashblocks) = flashblocks_state.as_ref() { + State::builder() + .with_database(cache_db) + .with_bundle_update() + .with_bundle_prestate(flashblocks.bundle_state.clone()) + .build() + } else { + State::builder() + .with_database(cache_db) + .with_bundle_update() + .build() + }; // Set up next block attributes // Use bundle.min_timestamp if provided, otherwise use header timestamp + BLOCK_TIME @@ -107,13 +167,40 @@ where }); } } + + // Calculate state root and measure its calculation time + db.merge_transitions(BundleRetention::Reverts); + let bundle = db.take_bundle(); + let state_provider = db.database.db.as_ref(); + + let state_root_start = Instant::now(); + let hashed_post_state = state_provider.hashed_post_state(&bundle); + + if let Some(cached) = cached_flashblock_trie { + // We have cached flashblock trie nodes, use them + let mut trie_input = TrieInput::from_state(hashed_post_state); + trie_input.prepend_cached(cached.trie_updates, cached.hashed_state); + let _ = state_provider.state_root_from_nodes_with_updates(trie_input)?; + } else if let Some((fb_trie_updates, fb_hashed_state)) = flashblock_trie { + // We computed the flashblock trie above, now use it + let mut trie_input = TrieInput::from_state(hashed_post_state); + trie_input.prepend_cached(fb_trie_updates, fb_hashed_state); + let _ = state_provider.state_root_from_nodes_with_updates(trie_input)?; + } else { + // No flashblocks, just calculate bundle state root + let _ = state_provider.state_root_with_updates(hashed_post_state)?; + } + + let state_root_time = state_root_start.elapsed().as_micros(); + let total_execution_time = execution_start.elapsed().as_micros(); - Ok(( + Ok(MeterBundleOutput { results, total_gas_used, total_gas_fees, bundle_hash, - total_execution_time, - )) + total_execution_time_us: total_execution_time, + state_root_time_us: state_root_time, + }) } diff --git a/crates/metering/src/rpc.rs b/crates/metering/src/rpc.rs index 053d4fa..a4bde6a 100644 --- a/crates/metering/src/rpc.rs +++ b/crates/metering/src/rpc.rs @@ -1,18 +1,20 @@ -use alloy_consensus::Header; +use alloy_consensus::{Header, Sealed}; use alloy_eips::eip2718::Decodable2718; -use alloy_eips::BlockNumberOrTag; use alloy_primitives::U256; +use base_reth_flashblocks_rpc::rpc::{FlashblocksAPI, PendingBlocksAPI}; use jsonrpsee::{ core::{async_trait, RpcResult}, proc_macros::rpc, }; use reth::providers::BlockReaderIdExt; use reth_optimism_chainspec::OpChainSpec; +use reth_primitives_traits::SealedHeader; use reth_provider::{ChainSpecProvider, StateProviderFactory}; +use std::sync::Arc; use tips_core::types::{Bundle, BundleWithMetadata, MeterBundleResponse}; use tracing::{error, info}; -use crate::meter_bundle; +use crate::{meter_bundle, FlashblockTrieCache}; /// RPC API for transaction metering #[rpc(server, namespace = "base")] @@ -23,25 +25,33 @@ pub trait MeteringApi { } /// Implementation of the metering RPC API -pub struct MeteringApiImpl { +pub struct MeteringApiImpl { provider: Provider, + flashblocks_state: Arc, + /// Single-entry cache for the latest flashblock's trie nodes + trie_cache: FlashblockTrieCache, } -impl MeteringApiImpl +impl MeteringApiImpl where Provider: StateProviderFactory + ChainSpecProvider + BlockReaderIdExt
+ Clone, + FB: FlashblocksAPI, { /// Creates a new instance of MeteringApi - pub fn new(provider: Provider) -> Self { - Self { provider } + pub fn new(provider: Provider, flashblocks_state: Arc) -> Self { + Self { + provider, + flashblocks_state, + trie_cache: FlashblockTrieCache::new(), + } } } #[async_trait] -impl MeteringApiServer for MeteringApiImpl +impl MeteringApiServer for MeteringApiImpl where Provider: StateProviderFactory + ChainSpecProvider @@ -50,6 +60,7 @@ where + Send + Sync + 'static, + FB: FlashblocksAPI + Send + Sync + 'static, { async fn meter_bundle(&self, bundle: Bundle) -> RpcResult { info!( @@ -58,24 +69,56 @@ where "Starting bundle metering" ); - // Get the latest header - let header = self - .provider - .sealed_header_by_number_or_tag(BlockNumberOrTag::Latest) - .map_err(|e| { - jsonrpsee::types::ErrorObjectOwned::owned( - jsonrpsee::types::ErrorCode::InternalError.code(), - format!("Failed to get latest header: {}", e), - None::<()>, - ) - })? - .ok_or_else(|| { - jsonrpsee::types::ErrorObjectOwned::owned( - jsonrpsee::types::ErrorCode::InternalError.code(), - "Latest block not found".to_string(), - None::<()>, - ) - })?; + // Get pending flashblocks state + let pending_blocks = self.flashblocks_state.get_pending_blocks(); + + // Get header and flashblock index from pending blocks + // If no pending blocks exist, fall back to latest canonical block + let (header, flashblock_index, canonical_block_number) = + if let Some(pb) = pending_blocks.as_ref() { + let latest_header: Sealed
= pb.latest_header(); + let flashblock_index = pb.latest_flashblock_index(); + let canonical_block_number = pb.canonical_block_number(); + + info!( + latest_block = latest_header.number, + canonical_block = %canonical_block_number, + flashblock_index = flashblock_index, + "Using latest flashblock state for metering" + ); + + // Convert Sealed
to SealedHeader + let sealed_header = + SealedHeader::new(latest_header.inner().clone(), latest_header.hash()); + (sealed_header, flashblock_index, canonical_block_number) + } else { + // No pending blocks, use latest canonical block + let canonical_block_number = pending_blocks.get_canonical_block_number(); + let header = self + .provider + .sealed_header_by_number_or_tag(canonical_block_number) + .map_err(|e| { + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + format!("Failed to get canonical block header: {}", e), + None::<()>, + ) + })? + .ok_or_else(|| { + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + "Canonical block not found".to_string(), + None::<()>, + ) + })?; + + info!( + canonical_block = header.number, + "No flashblocks available, using canonical block state for metering" + ); + + (header, 0, canonical_block_number) + }; // Manually decode transactions to OpTxEnvelope (op-alloy 0.20) instead of using // BundleWithMetadata.transactions() which returns op-alloy 0.21 types incompatible with reth. @@ -101,10 +144,10 @@ where ) })?; - // Get state provider for the block + // Get state provider for the canonical block let state_provider = self .provider - .state_by_block_hash(header.hash()) + .state_by_block_number_or_tag(canonical_block_number) .map_err(|e| { error!(error = %e, "Failed to get state provider"); jsonrpsee::types::ErrorObjectOwned::owned( @@ -114,49 +157,87 @@ where ) })?; + // If we have pending flashblocks, get the state to apply pending changes + let flashblocks_state = pending_blocks.as_ref().map(|pb| crate::FlashblocksState { + cache: pb.get_db_cache(), + bundle_state: pb.get_bundle_state(), + }); + + // Get the flashblock index if we have pending flashblocks + let state_flashblock_index = pending_blocks + .as_ref() + .map(|pb| pb.latest_flashblock_index()); + + // If we have flashblocks, ensure the trie is cached and get it + let cached_trie = if let Some(ref fb_state) = flashblocks_state { + let fb_index = state_flashblock_index.unwrap(); + + // Ensure the flashblock trie is cached and return it + Some( + self.trie_cache + .ensure_cached(header.hash(), fb_index, fb_state, &*state_provider) + .map_err(|e| { + error!(error = %e, "Failed to cache flashblock trie"); + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + format!("Failed to cache flashblock trie: {}", e), + None::<()>, + ) + })?, + ) + } else { + None + }; + // Meter bundle using utility function - let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = - meter_bundle( - state_provider, - self.provider.chain_spec().clone(), - decoded_txs, - &header, - &bundle_with_metadata, + let result = meter_bundle( + state_provider, + self.provider.chain_spec().clone(), + decoded_txs, + &header, + &bundle_with_metadata, + flashblocks_state, + cached_trie, + ) + .map_err(|e| { + error!(error = %e, "Bundle metering failed"); + jsonrpsee::types::ErrorObjectOwned::owned( + jsonrpsee::types::ErrorCode::InternalError.code(), + format!("Bundle metering failed: {}", e), + None::<()>, ) - .map_err(|e| { - error!(error = %e, "Bundle metering failed"); - jsonrpsee::types::ErrorObjectOwned::owned( - jsonrpsee::types::ErrorCode::InternalError.code(), - format!("Bundle metering failed: {}", e), - None::<()>, - ) - })?; + })?; // Calculate average gas price - let bundle_gas_price = if total_gas_used > 0 { - (total_gas_fees / U256::from(total_gas_used)).to_string() + let bundle_gas_price = if result.total_gas_used > 0 { + (result.total_gas_fees / U256::from(result.total_gas_used)).to_string() } else { "0".to_string() }; info!( - bundle_hash = %bundle_hash, - num_transactions = results.len(), - total_gas_used = total_gas_used, - total_execution_time_us = total_execution_time, + bundle_hash = %result.bundle_hash, + num_transactions = result.results.len(), + total_gas_used = result.total_gas_used, + total_execution_time_us = result.total_execution_time_us, + state_root_time_us = result.state_root_time_us, + state_block_number = header.number, + flashblock_index = flashblock_index, "Bundle metering completed successfully" ); Ok(MeterBundleResponse { bundle_gas_price, - bundle_hash, - coinbase_diff: total_gas_fees.to_string(), + bundle_hash: result.bundle_hash, + coinbase_diff: result.total_gas_fees.to_string(), eth_sent_to_coinbase: "0".to_string(), - gas_fees: total_gas_fees.to_string(), - results, + gas_fees: result.total_gas_fees.to_string(), + results: result.results, state_block_number: header.number, - total_gas_used, - total_execution_time_us: total_execution_time, + state_flashblock_index, + total_gas_used: result.total_gas_used, + total_execution_time_us: result.total_execution_time_us, + state_root_time_us: result.state_root_time_us, }) } } diff --git a/crates/metering/src/tests/meter.rs b/crates/metering/src/tests/meter.rs index dcec53f..d131b03 100644 --- a/crates/metering/src/tests/meter.rs +++ b/crates/metering/src/tests/meter.rs @@ -155,21 +155,23 @@ fn meter_bundle_empty_transactions() -> eyre::Result<()> { let bundle_with_metadata = create_bundle_with_metadata(Vec::new())?; - let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = - meter_bundle( - state_provider, - harness.chain_spec.clone(), - Vec::new(), - &harness.header, - &bundle_with_metadata, - )?; - - assert!(results.is_empty()); - assert_eq!(total_gas_used, 0); - assert_eq!(total_gas_fees, U256::ZERO); + let output = meter_bundle( + state_provider, + harness.chain_spec.clone(), + Vec::new(), + &harness.header, + &bundle_with_metadata, + None, + None, + )?; + + 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_execution_time_us > 0); + assert!(output.state_root_time_us > 0); + assert_eq!(output.bundle_hash, keccak256([])); Ok(()) } @@ -203,38 +205,40 @@ fn meter_bundle_single_transaction() -> eyre::Result<()> { let bundle_with_metadata = create_bundle_with_metadata(vec![envelope.clone()])?; - let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = - meter_bundle( - state_provider, - harness.chain_spec.clone(), - vec![envelope], - &harness.header, - &bundle_with_metadata, - )?; - - assert_eq!(results.len(), 1); - let result = &results[0]; - assert!(total_execution_time > 0); - - assert_eq!(result.from_address, harness.address(User::Alice)); - assert_eq!(result.to_address, Some(to)); - assert_eq!(result.tx_hash, tx_hash); - assert_eq!(result.gas_price, U256::from(10).to_string()); - assert_eq!(result.gas_used, 21_000); + let output = meter_bundle( + state_provider, + harness.chain_spec.clone(), + vec![envelope], + &harness.header, + &bundle_with_metadata, + None, + None, + )?; + + assert_eq!(output.results.len(), 1); + let tx_result = &output.results[0]; + assert!(output.total_execution_time_us > 0); + assert!(output.state_root_time_us > 0); + + assert_eq!(tx_result.from_address, harness.address(User::Alice)); + assert_eq!(tx_result.to_address, Some(to)); + assert_eq!(tx_result.tx_hash, tx_hash); + assert_eq!(tx_result.gas_price, U256::from(10).to_string()); + assert_eq!(tx_result.gas_used, 21_000); assert_eq!( - result.coinbase_diff, + tx_result.coinbase_diff, (U256::from(21_000) * U256::from(10)).to_string(), ); - 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, + tx_result.execution_time_us > 0, "execution_time_us should be greater than zero" ); @@ -299,62 +303,119 @@ fn meter_bundle_multiple_transactions() -> eyre::Result<()> { let bundle_with_metadata = create_bundle_with_metadata(vec![envelope_1.clone(), envelope_2.clone()])?; - let (results, total_gas_used, total_gas_fees, bundle_hash, total_execution_time) = - meter_bundle( - state_provider, - harness.chain_spec.clone(), - vec![envelope_1, envelope_2], - &harness.header, - &bundle_with_metadata, - )?; + let output = meter_bundle( + state_provider, + harness.chain_spec.clone(), + vec![envelope_1, envelope_2], + &harness.header, + &bundle_with_metadata, + None, + None, + )?; - assert_eq!(results.len(), 2); - assert!(total_execution_time > 0); + assert_eq!(output.results.len(), 2); + assert!(output.total_execution_time_us > 0); + assert!(output.state_root_time_us > 0); // Check first transaction - let result_1 = &results[0]; - assert_eq!(result_1.from_address, harness.address(User::Alice)); - assert_eq!(result_1.to_address, Some(to_1)); - assert_eq!(result_1.tx_hash, tx_hash_1); - assert_eq!(result_1.gas_price, U256::from(10).to_string()); - assert_eq!(result_1.gas_used, 21_000); + let tx_result_1 = &output.results[0]; + assert_eq!(tx_result_1.from_address, harness.address(User::Alice)); + assert_eq!(tx_result_1.to_address, Some(to_1)); + assert_eq!(tx_result_1.tx_hash, tx_hash_1); + assert_eq!(tx_result_1.gas_price, U256::from(10).to_string()); + assert_eq!(tx_result_1.gas_used, 21_000); assert_eq!( - result_1.coinbase_diff, + tx_result_1.coinbase_diff, (U256::from(21_000) * U256::from(10)).to_string(), ); // Check second transaction - let result_2 = &results[1]; - assert_eq!(result_2.from_address, harness.address(User::Bob)); - assert_eq!(result_2.to_address, Some(to_2)); - assert_eq!(result_2.tx_hash, tx_hash_2); - assert_eq!(result_2.gas_price, U256::from(15).to_string()); - assert_eq!(result_2.gas_used, 21_000); + let tx_result_2 = &output.results[1]; + assert_eq!(tx_result_2.from_address, harness.address(User::Bob)); + assert_eq!(tx_result_2.to_address, Some(to_2)); + assert_eq!(tx_result_2.tx_hash, tx_hash_2); + assert_eq!(tx_result_2.gas_price, U256::from(15).to_string()); + assert_eq!(tx_result_2.gas_used, 21_000); assert_eq!( - result_2.coinbase_diff, + tx_result_2.coinbase_diff, (U256::from(21_000) * U256::from(15)).to_string(), ); // 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, + tx_result_1.execution_time_us > 0, "execution_time_us should be greater than zero" ); assert!( - result_2.execution_time_us > 0, + tx_result_2.execution_time_us > 0, "execution_time_us should be greater than zero" ); Ok(()) } + +#[test] +fn meter_bundle_state_root_time_invariant() -> eyre::Result<()> { + let harness = setup_harness()?; + + let to = Address::random(); + let signed_tx = TransactionBuilder::default() + .signer(harness.signer(User::Alice)) + .chain_id(harness.chain_spec.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 envelope = envelope_from_signed(&tx)?; + + let state_provider = harness + .provider + .state_by_block_hash(harness.header.hash()) + .context("getting state provider")?; + + let bundle_with_metadata = create_bundle_with_metadata(vec![envelope.clone()])?; + + let output = meter_bundle( + state_provider, + harness.chain_spec.clone(), + vec![envelope], + &harness.header, + &bundle_with_metadata, + None, + None, + )?; + + // Verify invariant: total execution time must include state root time + assert!( + output.total_execution_time_us >= output.state_root_time_us, + "total_execution_time_us ({}) should be >= state_root_time_us ({})", + output.total_execution_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/metering/src/tests/rpc.rs b/crates/metering/src/tests/rpc.rs index 1beea0c..1d1449b 100644 --- a/crates/metering/src/tests/rpc.rs +++ b/crates/metering/src/tests/rpc.rs @@ -6,6 +6,7 @@ mod tests { use alloy_primitives::bytes; use alloy_primitives::{address, b256, Bytes, U256}; use alloy_rpc_client::RpcClient; + use base_reth_flashblocks_rpc::state::FlashblocksState; use op_alloy_consensus::OpTxEnvelope; use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; use reth::builder::{Node, NodeBuilder, NodeConfig, NodeHandle}; @@ -91,7 +92,9 @@ mod tests { .with_components(node.components_builder()) .with_add_ons(node.add_ons()) .extend_rpc_modules(move |ctx| { - let metering_api = MeteringApiImpl::new(ctx.provider().clone()); + // Create a FlashblocksState without starting it (no pending blocks for testing) + let flashblocks_state = Arc::new(FlashblocksState::new(ctx.provider().clone())); + let metering_api = MeteringApiImpl::new(ctx.provider().clone(), flashblocks_state); ctx.modules.merge_configured(metering_api.into_rpc())?; Ok(()) }) @@ -170,6 +173,18 @@ mod tests { assert_eq!(response.total_gas_used, 21_000); assert!(response.total_execution_time_us > 0); + // Verify state root time is present and non-zero + assert!( + response.state_root_time_us > 0, + "state_root_time_us should be greater than zero" + ); + + // Verify invariant: total execution time includes state root time + assert!( + response.total_execution_time_us >= response.state_root_time_us, + "total_execution_time_us should be >= state_root_time_us" + ); + let result = &response.results[0]; assert_eq!(result.from_address, sender_address); assert_eq!( diff --git a/crates/node/src/main.rs b/crates/node/src/main.rs index 3c0e574..9d89218 100644 --- a/crates/node/src/main.rs +++ b/crates/node/src/main.rs @@ -138,12 +138,6 @@ fn main() { } }) .extend_rpc_modules(move |ctx| { - if metering_enabled { - info!(message = "Starting Metering RPC"); - let metering_api = MeteringApiImpl::new(ctx.provider().clone()); - ctx.modules.merge_configured(metering_api.into_rpc())?; - } - if flashblocks_enabled { info!(message = "Starting Flashblocks"); @@ -164,12 +158,23 @@ fn main() { let api_ext = EthApiExt::new( ctx.registry.eth_api().clone(), ctx.registry.eth_handlers().filter.clone(), - fb, + fb.clone(), ); ctx.modules.replace_configured(api_ext.into_rpc())?; + + if metering_enabled { + info!(message = "Starting Metering RPC with Flashblocks state"); + let metering_api = MeteringApiImpl::new(ctx.provider().clone(), fb); + ctx.modules.merge_configured(metering_api.into_rpc())?; + } } else { info!(message = "flashblocks integration is disabled"); + if metering_enabled { + return Err(eyre::eyre!( + "Metering RPC requires flashblocks to be enabled (--websocket-url)" + )); + } } Ok(()) })