From 268919c0f156d084424a0572ec8739c2eaa0b615 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Mon, 31 Aug 2026 13:14:03 -0300 Subject: [PATCH 01/31] access_list --- .../executor/evm/types/output/access_list.rs | 4 ++- src/eth/executor/mod.rs | 6 +++- src/eth/follower/consensus.rs | 15 ++++++-- src/eth/follower/importer/importer_config.rs | 1 + .../follower/importer/importer_supervisor.rs | 9 +++-- .../importer/importers/fake_leader.rs | 2 +- .../blockchain_client/blockchain_client.rs | 8 ++--- src/eth/rpc/middleware/rpc_middleware.rs | 10 +++++- src/eth/rpc/server.rs | 8 ++--- src/eth/storage/cache.rs | 6 ++-- .../permanent/rocks/rocks_permanent.rs | 5 +++ .../storage/permanent/rocks/rocks_state.rs | 11 ++++++ src/eth/storage/stratus_storage.rs | 34 ++++++++++++++++++- src/eth/types/transaction/call_input.rs | 12 +++++++ 14 files changed, 110 insertions(+), 21 deletions(-) diff --git a/src/eth/executor/evm/types/output/access_list.rs b/src/eth/executor/evm/types/output/access_list.rs index 01db28fde..298ee7c67 100644 --- a/src/eth/executor/evm/types/output/access_list.rs +++ b/src/eth/executor/evm/types/output/access_list.rs @@ -1,3 +1,4 @@ +use derive_more::IntoIterator; use display_json::DebugAsJson; use revm_state::EvmState; @@ -6,8 +7,9 @@ use crate::eth::types::Address; use crate::eth::types::SlotIndex; use crate::eth::types::StratusError; -#[derive(serde::Serialize, DebugAsJson)] +#[derive(serde::Serialize, serde::Deserialize, DebugAsJson, Clone, IntoIterator)] pub struct AccessListOutput { + #[into_iterator(owned, ref, ref_mut)] access_list: Vec<(Address, Vec)>, } diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index d5c022af1..35b5359af 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -269,7 +269,7 @@ impl Executor { /// Executes a transaction persisting state changes. #[tracing::instrument(name = "executor::local_transaction", skip_all, fields(tx_hash, tx_from, tx_to, tx_nonce))] - pub fn execute_local_transaction(&self, tx: TransactionInput) -> Result<(), StratusError> { + pub fn execute_local_transaction(&self, tx: TransactionInput, access_list: Option) -> Result<(), StratusError> { #[cfg(feature = "metrics")] let function = codegen::function_sig(&tx.execution_info.input); #[cfg(feature = "metrics")] @@ -285,6 +285,10 @@ impl Executor { s.rec_str("tx_nonce", &tx.execution_info.nonce); }); + if let Some(access_list) = access_list { + self.storage.load_access_list(access_list); + } + // execute according to the strategy const INFINITE_ATTEMPTS: usize = usize::MAX; diff --git a/src/eth/follower/consensus.rs b/src/eth/follower/consensus.rs index fc29d0b96..d3c756790 100644 --- a/src/eth/follower/consensus.rs +++ b/src/eth/follower/consensus.rs @@ -2,11 +2,14 @@ use std::sync::Arc; use strum::AsRefStr; +use crate::eth::executor::AccessListOutput; +use crate::eth::executor::Executor; use crate::eth::rpc::BlockchainClient; use crate::eth::rpc::RpcClientApp; use crate::eth::types::Bytes; use crate::eth::types::Hash; use crate::eth::types::StratusError; +use crate::eth::types::TransactionInput; #[cfg(feature = "metrics")] use crate::infra::metrics; @@ -59,13 +62,17 @@ pub trait Consensus: Send + Sync { } /// Forwards a transaction to leader. - async fn forward_to_leader(&self, tx_hash: Hash, tx_data: Bytes, rpc_client: &RpcClientApp) -> Result { + async fn forward_to_leader(&self, tx: TransactionInput, tx_hash: Hash, tx_data: Bytes, rpc_client: &RpcClientApp) -> Result { #[cfg(feature = "metrics")] let start = metrics::now(); tracing::info!(%tx_hash, %rpc_client, "forwarding transaction to leader"); - let hash = self.get_chain()?.send_raw_transaction_to_leader(tx_data.into(), rpc_client).await?; + let access_list = self + .get_executor() + .execute_local_call::(tx.into(), crate::eth::types::PointInTime::Latest)?; + + let hash = self.get_client().send_raw_transaction_to_leader(tx_data.into(), Some(access_list)).await?; #[cfg(feature = "metrics")] metrics::inc_consensus_forward(start.elapsed()); @@ -73,7 +80,9 @@ pub trait Consensus: Send + Sync { Ok(hash) } - fn get_chain(&self) -> anyhow::Result<&Arc>; + fn get_client(&self) -> &Arc; + + fn get_executor(&self) -> &Arc; /// Get the lag status between this node and the leader. async fn lag(&self) -> anyhow::Result; diff --git a/src/eth/follower/importer/importer_config.rs b/src/eth/follower/importer/importer_config.rs index 243b5df02..84855ea11 100644 --- a/src/eth/follower/importer/importer_config.rs +++ b/src/eth/follower/importer/importer_config.rs @@ -110,6 +110,7 @@ impl ImporterConfig { let consensus = Arc::new(ImporterConsensus { storage: Arc::clone(&storage), chain: Arc::clone(&chain), + executor: Arc::clone(&executor), }); spawn( diff --git a/src/eth/follower/importer/importer_supervisor.rs b/src/eth/follower/importer/importer_supervisor.rs index 44046529e..78d1cbbd4 100644 --- a/src/eth/follower/importer/importer_supervisor.rs +++ b/src/eth/follower/importer/importer_supervisor.rs @@ -151,6 +151,7 @@ pub async fn start_importer( pub struct ImporterConsensus { pub storage: Arc, pub chain: Arc, + pub executor: Arc, } impl Consensus for ImporterConsensus { @@ -183,7 +184,11 @@ impl Consensus for ImporterConsensus { } } - fn get_chain(&self) -> anyhow::Result<&Arc> { - Ok(&self.chain) + fn get_client(&self) -> &Arc { + &self.chain + } + + fn get_executor(&self) -> &Arc { + &self.executor } } diff --git a/src/eth/follower/importer/importers/fake_leader.rs b/src/eth/follower/importer/importers/fake_leader.rs index b43b9282d..9cc1449f0 100644 --- a/src/eth/follower/importer/importers/fake_leader.rs +++ b/src/eth/follower/importer/importers/fake_leader.rs @@ -39,7 +39,7 @@ impl ImporterWorker for FakeLeaderWorker { self.storage.set_pending_from_external(&block); for tx in block.0.transactions.into_transactions() { tracing::info!(?tx, "executing tx as fake miner"); - if let Err(e) = self.executor.execute_local_transaction(tx.try_into()?) { + if let Err(e) = self.executor.execute_local_transaction(tx.try_into()?, None) { match e { StratusError::Executor(ExecutorError::Nonce { transaction: _, account: _ }) => { tracing::warn!(reason = ?e, "transaction failed, was this node restarted?"); diff --git a/src/eth/rpc/blockchain_client/blockchain_client.rs b/src/eth/rpc/blockchain_client/blockchain_client.rs index 9f4aa6854..2ca5d280e 100644 --- a/src/eth/rpc/blockchain_client/blockchain_client.rs +++ b/src/eth/rpc/blockchain_client/blockchain_client.rs @@ -17,8 +17,8 @@ use crate::GlobalState; use crate::alias::AlloyBytes; use crate::alias::AlloyTransaction; use crate::alias::JsonValue; +use crate::eth::executor::AccessListOutput; use crate::eth::executor::ExecutorError; -use crate::eth::rpc::RpcClientApp; use crate::eth::storage::permanent::rocks::types::BlockChangesRocksdb; use crate::eth::storage::permanent::rocks::types::BlockRocksdb; use crate::eth::types::Address; @@ -249,12 +249,12 @@ impl BlockchainClient { // ------------------------------------------------------------------------- /// Forwards a transaction to leader. - pub async fn send_raw_transaction_to_leader(&self, tx: AlloyBytes, rpc_client: &RpcClientApp) -> Result { + pub async fn send_raw_transaction_to_leader(&self, tx: AlloyBytes, access_list: Option) -> Result { tracing::debug!("sending raw transaction to leader"); let tx = to_json_value(tx); - let rpc_client = to_json_value(rpc_client); - let result = self.http.request::("eth_sendRawTransaction", [tx, rpc_client]).await; + let access_list = to_json_value(access_list); + let result = self.http.request::("eth_sendRawTransaction", [tx, access_list]).await; match result { Ok(hash) => Ok(hash), diff --git a/src/eth/rpc/middleware/rpc_middleware.rs b/src/eth/rpc/middleware/rpc_middleware.rs index 74d4a7e90..9c0795bd3 100644 --- a/src/eth/rpc/middleware/rpc_middleware.rs +++ b/src/eth/rpc/middleware/rpc_middleware.rs @@ -29,6 +29,7 @@ use crate::alias::JsonValue; use crate::eth::codegen; use crate::eth::codegen::ContractName; use crate::eth::codegen::SoliditySignature; +use crate::eth::executor::AccessListOutput; use crate::eth::rpc::RpcClientApp; use crate::eth::rpc::RpcError; use crate::eth::rpc::middleware::multicall::MulticallInfo; @@ -189,7 +190,13 @@ impl RpcServiceT for RpcMiddleware { if method == "eth_sendRawTransaction" { let tx_data_result = next_rpc_param::(params_clone.sequence()); - if let Ok((_, tx_data)) = tx_data_result { + + if let Ok((next_param, tx_data)) = tx_data_result { + let access_list = next_rpc_param::>(next_param) + .map(|(_params, access_list)| access_list) + .inspect_err(|err| tracing::warn!(?err, "failed to deserialize access list")) + .ok() + .flatten(); let decoded_tx_result = parse_rpc_rlp::(&tx_data); if let Ok(decoded_tx) = decoded_tx_result { @@ -197,6 +204,7 @@ impl RpcServiceT for RpcMiddleware { request.extensions_mut().insert(tx_data); request.extensions_mut().insert(decoded_tx); + request.extensions_mut().insert(access_list); } } } else { diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index 5c5d476a3..858eeeeb3 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -1315,8 +1315,8 @@ fn eth_send_raw_transaction(_: Params<'_>, ctx: Arc, ext: Extensions .entered(); // get the pre-decoded transaction from extensions - let (tx, tx_data) = match (ext.get::(), ext.get::()) { - (Some(tx), Some(data)) => (tx.clone(), data.clone()), + let (tx, tx_data, access_list) = match (ext.get::(), ext.get::(), ext.get::>()) { + (Some(tx), Some(data), access_list) => (tx.clone(), data.clone(), access_list.cloned().flatten()), _ => { tracing::error!("failed to execute eth_sendRawTransaction because transaction input is not available"); return Err(RpcError::TransactionInvalid { @@ -1350,7 +1350,7 @@ fn eth_send_raw_transaction(_: Params<'_>, ctx: Arc, ext: Extensions // execute locally or forward to leader match GlobalState::get_node_mode() { - NodeMode::Leader | NodeMode::FakeLeader => match ctx.server.executor.execute_local_transaction(tx) { + NodeMode::Leader | NodeMode::FakeLeader => match ctx.server.executor.execute_local_transaction(tx, access_list) { Ok(_) => Ok(hex_data(tx_hash)), Err(e) => { tracing::warn!(reason = ?e, ?tx_hash, "failed to execute eth_sendRawTransaction"); @@ -1358,7 +1358,7 @@ fn eth_send_raw_transaction(_: Params<'_>, ctx: Arc, ext: Extensions } }, NodeMode::Follower => match &ctx.server.read_importer() { - Some(importer) => match Handle::current().block_on(importer.forward_to_leader(tx_hash, tx_data, ext.rpc_client())) { + Some(importer) => match Handle::current().block_on(importer.forward_to_leader(tx, tx_hash, tx_data, ext.rpc_client())) { Ok(hash) => Ok(hex_data(hash)), Err(e) => Err(e), }, diff --git a/src/eth/storage/cache.rs b/src/eth/storage/cache.rs index 89d883e23..29aa39696 100644 --- a/src/eth/storage/cache.rs +++ b/src/eth/storage/cache.rs @@ -1,4 +1,5 @@ use std::hash::Hash; +use std::time::Duration; use clap::Parser; use display_json::DebugAsJson; @@ -160,13 +161,12 @@ where L: quick_cache::Lifecycle + Clone, { fn insert_if_missing(&self, key: Key, val: Val) { - match self.get_value_or_guard(&key, None) { - GuardResult::Value(_) => (), + match self.get_value_or_guard(&key, Some(Duration::ZERO)) { + GuardResult::Value(_) | GuardResult::Timeout => (), GuardResult::Guard(g) => { // this fails if an unguarded insert already inserted to this key let _ = g.insert(val); } - GuardResult::Timeout => unreachable!(), } } } diff --git a/src/eth/storage/permanent/rocks/rocks_permanent.rs b/src/eth/storage/permanent/rocks/rocks_permanent.rs index cb3b34c3d..0fba2dfbb 100644 --- a/src/eth/storage/permanent/rocks/rocks_permanent.rs +++ b/src/eth/storage/permanent/rocks/rocks_permanent.rs @@ -28,6 +28,7 @@ use crate::eth::types::LogMessage; use crate::eth::types::Nonce; use crate::eth::types::Slot; use crate::eth::types::SlotIndex; +use crate::eth::types::SlotValue; use crate::eth::types::TransactionMined; #[cfg(feature = "dev")] use crate::eth::types::Wei; @@ -176,6 +177,10 @@ impl RocksPermanentStorage { }) } + pub fn read_slots(&self, slot_keys: Vec<(Address, SlotIndex)>) -> anyhow::Result, StorageError> { + self.state.read_slots(slot_keys).map_err(|err| StorageError::RocksError { err }) + } + pub fn read_block(&self, selection: BlockFilter) -> anyhow::Result, StorageError> { let block = self.state.read_block(selection).inspect_err(|e| { tracing::error!(reason = ?e, "failed to read block in RocksPermanent"); diff --git a/src/eth/storage/permanent/rocks/rocks_state.rs b/src/eth/storage/permanent/rocks/rocks_state.rs index 142d05679..21ae3e4cb 100644 --- a/src/eth/storage/permanent/rocks/rocks_state.rs +++ b/src/eth/storage/permanent/rocks/rocks_state.rs @@ -60,6 +60,7 @@ use crate::eth::types::LogMessage; use crate::eth::types::Nonce; use crate::eth::types::Slot; use crate::eth::types::SlotIndex; +use crate::eth::types::SlotValue; use crate::eth::types::TransactionMined; #[cfg(feature = "dev")] use crate::eth::types::Wei; @@ -363,6 +364,16 @@ impl RocksStorageState { } } + pub fn read_slots(&self, slot_keys: Vec<(Address, SlotIndex)>) -> Result> { + self.account_slots + .multi_get(slot_keys.into_iter().map(|(address, index)| (address.into(), index.into()))) + .map(|vec| { + vec.into_iter() + .map(|((address, index), slot_value)| ((address.into(), index.into()), slot_value.into_inner().into())) + .collect_vec() + }) + } + pub fn read_account(&self, address: Address, point: &MinedPointInTime<'_>) -> Result> { if address.is_coinbase() || address.is_zero() { return Ok(None); diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index 9c00c9cab..3c93eb11b 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -1,5 +1,8 @@ +use std::collections::HashMap; + use tracing::Span; +use crate::eth::executor::AccessListOutput; use crate::eth::executor::AccountOriginalsReader; use crate::eth::executor::Changes; use crate::eth::executor::TransactionExecution; @@ -32,7 +35,6 @@ use crate::eth::types::PendingBlockHeader; use crate::eth::types::PointInTime; use crate::eth::types::Slot; use crate::eth::types::SlotIndex; -#[cfg(feature = "dev")] use crate::eth::types::SlotValue; use crate::eth::types::TransactionStage; use crate::eth::types::UnixTime; @@ -375,6 +377,7 @@ impl StratusStorage { // Accounts and slots // ------------------------------------------------------------------------- + #[cfg(feature = "dev")] pub fn save_accounts(&self, accounts: Vec) -> Result<(), StorageError> { #[cfg(feature = "tracing")] let _span = tracing::info_span!("storage::save_accounts").entered(); @@ -844,6 +847,35 @@ impl StratusStorage { }, } } + + fn load_slots_to_cache(&self, slots: Vec<(Address, SlotIndex)>) { + let existing_slots: HashMap<(Address, SlotIndex), SlotValue> = self.perm.read_slots(slots.clone()).unwrap().into_iter().collect(); + for (address, index) in slots { + let value = existing_slots.get(&(address, index)).copied().unwrap_or_default(); + Slot::cache_latest_if_missing(self, (address, index), Slot { index, value }); + } + } + + fn load_accounts_to_cache(&self, addresses: Vec
) { + let existing_accounts: HashMap = self.perm.read_accounts(addresses.clone()).unwrap().into_iter().collect(); + for address in addresses { + let account = existing_accounts.get(&address).cloned().unwrap_or_default(); + Account::cache_latest_if_missing(self, address, account); + } + } + + pub fn load_access_list(&self, access_list: AccessListOutput) { + let mut account_addresses = vec![]; + let mut slot_keys = vec![]; + for (address, slots) in access_list { + account_addresses.push(address); + for slot_index in slots { + slot_keys.push((address, slot_index)); + } + } + self.load_accounts_to_cache(account_addresses); + self.load_slots_to_cache(slot_keys); + } } #[cfg(test)] diff --git a/src/eth/types/transaction/call_input.rs b/src/eth/types/transaction/call_input.rs index 26291031a..26e62e5af 100644 --- a/src/eth/types/transaction/call_input.rs +++ b/src/eth/types/transaction/call_input.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use crate::eth::types::Address; use crate::eth::types::Bytes; +use crate::eth::types::TransactionInput; use crate::eth::types::Wei; #[derive(DebugAsJson, Clone, PartialEq, Eq, serde::Serialize)] @@ -39,3 +40,14 @@ impl<'de> Deserialize<'de> for CallInput { Ok(CallInput { from, to, value, data }) } } + +impl From for CallInput { + fn from(value: TransactionInput) -> Self { + Self { + from: Some(value.signer()), + to: value.execution_info.to, + value: value.execution_info.value, + data: value.execution_info.input, + } + } +} From 65fb3b5aea6a51de672d14a578765f83ee6c9995 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Mon, 31 Aug 2026 22:56:29 -0300 Subject: [PATCH 02/31] try caching only missing values --- src/eth/storage/cache.rs | 64 ++++++++++++++++++++++++++++-- src/eth/storage/stratus_storage.rs | 26 +++++++++--- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/eth/storage/cache.rs b/src/eth/storage/cache.rs index a5af99168..1dfbbefb2 100644 --- a/src/eth/storage/cache.rs +++ b/src/eth/storage/cache.rs @@ -85,12 +85,35 @@ impl StorageCache { self.slot_latest_cache.insert_if_missing((address, slot.index), slot.value); } - pub fn get_account_latest(&self, address: Address) -> Option { - self.account_latest_cache.get(&address) + pub fn get_account_latest(&self, address: &Address) -> Option { + self.account_latest_cache.get(address) } - pub fn get_slot_latest(&self, address: Address, index: SlotIndex) -> Option { - self.slot_latest_cache.get(&(address, index)).map(|value| Slot { value, index }) + pub fn get_slot_latest(&self, address: &Address, index: &SlotIndex) -> Option { + self.slot_latest_cache + .get(&SlotKeyRef(address, index)) + .map(|value| Slot { value, index: *index }) + } + + pub fn contains_account(&self, address: &Address) -> bool { + self.account_latest_cache.contains_key(address) + } + + pub fn contains_slot(&self, address: &Address, index: &SlotIndex) -> bool { + self.slot_latest_cache.contains_key(&SlotKeyRef(address, index)) + } +} + +// Borrowed lookup key for `slot_latest_cache`. `std` provides no `Borrow<(&A, &B)>` impl for `(A, B)`, so a tuple of +// references cannot be used directly with `get`/`contains_key`; and the orphan rule (E0117) forbids implementing +// `Equivalent` for `(&Address, &SlotIndex)` directly because tuples are always foreign. The derived `Hash` hashes the +// fields in the same order as the `(Address, SlotIndex)` tuple, which is required for the lookups to match. +#[derive(Hash)] +struct SlotKeyRef<'a>(&'a Address, &'a SlotIndex); + +impl Equivalent<(Address, SlotIndex)> for SlotKeyRef<'_> { + fn equivalent(&self, key: &(Address, SlotIndex)) -> bool { + self.0 == &key.0 && self.1 == &key.1 } } @@ -120,3 +143,36 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn new_cache() -> StorageCache { + CacheConfig { + account_history_cache_capacity: 16, + slot_history_cache_capacity: 16, + } + .init() + } + + #[test] + fn contains_slot_finds_cached_slots() { + let cache = new_cache(); + let address = Address::new([0xAA; 20]); + let index = SlotIndex::from([7u64, 0, 0, 0]); + let other_index = SlotIndex::from([8u64, 0, 0, 0]); + let other_address = Address::new([0xBB; 20]); + + cache.cache_slot_latest_if_missing(address, Slot::new(index, SlotValue::from([42u64, 0, 0, 0]))); + + let expected_slot = Slot::new(index, SlotValue::from([42u64, 0, 0, 0])); + assert_eq!(cache.get_slot_latest(&address, &index), Some(expected_slot)); + assert_eq!(cache.get_slot_latest(&address, &other_index), None); + assert_eq!(cache.get_slot_latest(&other_address, &index), None); + + assert!(cache.contains_slot(&address, &index)); + assert!(!cache.contains_slot(&address, &other_index)); + assert!(!cache.contains_slot(&other_address, &index)); + } +} diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index fc4ac6a84..1754da134 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -93,7 +93,9 @@ enum FoundAt { pub(super) trait EntityRead: Sized + Clone { type Key: Copy; /// Reads the latest (mined tip) value from the cache, if present. - fn read_latest_cache(s: &StratusStorage, key: Self::Key) -> Option; + fn read_latest_cache(s: &StratusStorage, key: &Self::Key) -> Option; + /// Checks if the latest cache contains the given key. + fn cache_contains_key(s: &StratusStorage, key: &Self::Key) -> bool; /// Reads from temporary (pending) storage. fn read_temp(s: &StratusStorage, key: Self::Key) -> Option; /// Reads from permanent storage at the resolved mined point. @@ -114,7 +116,7 @@ impl EntityRead for Account { }) } - fn read_latest_cache(s: &StratusStorage, address: Address) -> Option { + fn read_latest_cache(s: &StratusStorage, address: &Address) -> Option { timed(|| s.cache.get_account_latest(address)).with(|m| { if m.result.is_some() { tracing::debug!(storage = %label::CACHE, %address, "account found in cache"); @@ -123,6 +125,10 @@ impl EntityRead for Account { }) } + fn cache_contains_key(s: &StratusStorage, key: &Self::Key) -> bool { + s.cache.contains_account(key) + } + fn read_perm(s: &StratusStorage, address: Address, point: MinedPointInTime<'_>) -> Result { tracing::debug!(storage = %label::PERM, %address, "reading account"); let account = timed(|| s.perm.read_account(address, &point)).with(|m| { @@ -164,7 +170,7 @@ impl EntityRead for Slot { }) } - fn read_latest_cache(s: &StratusStorage, key: (Address, SlotIndex)) -> Option { + fn read_latest_cache(s: &StratusStorage, key: &(Address, SlotIndex)) -> Option { let (address, index) = key; timed(|| s.cache.get_slot_latest(address, index)).with(|m| { if m.result.is_some() { @@ -174,6 +180,10 @@ impl EntityRead for Slot { }) } + fn cache_contains_key(s: &StratusStorage, key: &Self::Key) -> bool { + s.cache.contains_slot(&key.0, &key.1) + } + fn read_perm(s: &StratusStorage, key: (Address, SlotIndex), point: MinedPointInTime<'_>) -> Result { let (address, index) = key; tracing::debug!(storage = %label::PERM, %address, %index, %point, "reading slot"); @@ -367,7 +377,7 @@ impl StratusStorage { MinedPointInTime::Latest(_, _) => // Latest: try latest cache while guard is held, then fall through to perm. { - if let Some(value) = E::read_latest_cache(self, key) { + if let Some(value) = E::read_latest_cache(self, &key) { break 'query (value, FoundAt::Cache); } // If it wasnt found in the cache and we still have the guard the value can only be read in perm latest @@ -813,9 +823,13 @@ impl StratusStorage { let mut account_addresses = vec![]; let mut slot_keys = vec![]; for (address, slots) in access_list { - account_addresses.push(address); + if !Account::cache_contains_key(self, &address) { + account_addresses.push(address); + } for slot_index in slots { - slot_keys.push((address, slot_index)); + if !Slot::cache_contains_key(self, &(address, slot_index)) { + slot_keys.push((address, slot_index)); + } } } self.load_accounts_to_cache(account_addresses); From 79980bc42e302a82c7310f87e836b47eb6d35d91 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Mon, 31 Aug 2026 23:06:40 -0300 Subject: [PATCH 03/31] also check temp --- src/eth/storage/stratus_storage.rs | 4 ++-- .../storage/temporary/inmemory/transaction.rs | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index 1754da134..b385a08bc 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -126,7 +126,7 @@ impl EntityRead for Account { } fn cache_contains_key(s: &StratusStorage, key: &Self::Key) -> bool { - s.cache.contains_account(key) + s.temp.transaction_storage.contains_account(key) || s.cache.contains_account(key) } fn read_perm(s: &StratusStorage, address: Address, point: MinedPointInTime<'_>) -> Result { @@ -181,7 +181,7 @@ impl EntityRead for Slot { } fn cache_contains_key(s: &StratusStorage, key: &Self::Key) -> bool { - s.cache.contains_slot(&key.0, &key.1) + s.temp.transaction_storage.contains_slot(key) || s.cache.contains_slot(&key.0, &key.1) } fn read_perm(s: &StratusStorage, key: (Address, SlotIndex), point: MinedPointInTime<'_>) -> Result { diff --git a/src/eth/storage/temporary/inmemory/transaction.rs b/src/eth/storage/temporary/inmemory/transaction.rs index 94a4c7194..d319dd8f1 100644 --- a/src/eth/storage/temporary/inmemory/transaction.rs +++ b/src/eth/storage/temporary/inmemory/transaction.rs @@ -178,6 +178,30 @@ impl InmemoryTransactionTemporaryStorage { } } + pub fn contains_account(&self, address: &Address) -> bool { + match self.pending_block.read().state.accounts.contains_key(&address) { + true => true, + false => self + .latest_block + .read() + .as_ref() + .map(|latest| latest.state.accounts.contains_key(&address)) + .unwrap_or(false), + } + } + + pub fn contains_slot(&self, slot_key: &(Address, SlotIndex)) -> bool { + match self.pending_block.read().state.slots.contains_key(slot_key) { + true => true, + false => self + .latest_block + .read() + .as_ref() + .map(|latest| latest.state.slots.contains_key(slot_key)) + .unwrap_or(false), + } + } + // ------------------------------------------------------------------------- // Direct state manipulation (for testing) // ------------------------------------------------------------------------- From 9ff339a54c20f193b796d0eef9fd5fe07e3bf557 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 02:59:51 -0300 Subject: [PATCH 04/31] warmup semaphore lock --- src/eth/executor/mod.rs | 62 +++++++++++++++++++++--- src/infra/metrics/metrics_definitions.rs | 4 +- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index 30a5e2f09..6e61a1cdd 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -12,6 +12,7 @@ use alloy_rpc_types_trace::geth::GethDebugTracingOptions; use alloy_rpc_types_trace::geth::GethTrace; use anyhow::bail; pub use config::ExecutorConfig; +use derive_more::Deref; pub use evm::types::AccessListOutput; pub use evm::types::CallExecutionOutput; pub use evm::types::EvmExecutionMetrics; @@ -19,6 +20,7 @@ pub use evm::types::EvmKind; pub use evm::types::TransactionExecutionInput; pub use evm::types::TransactionExecutionOutput; pub use evm::types::TransactionExecutionResult; +use parking_lot::Condvar; use parking_lot::Mutex; use tracing::Span; use tracing::debug_span; @@ -66,10 +68,56 @@ use crate::infra::tracing::SpanExt; // Executor // ----------------------------------------------------------------------------- +#[derive(Deref, Default)] +struct Semaphore { + #[deref] + sem: Arc, +} + +#[derive(Default)] +struct SemaphoreInner { + permits: Mutex, + cvar: Condvar, +} + +struct Permit { + sem: Arc, +} + +impl Semaphore { + fn new(permits: usize) -> Self { + Self { + sem: Arc::new(SemaphoreInner { + permits: Mutex::new(permits), + cvar: Condvar::new(), + }), + } + } + + fn acquire(&self) -> Permit { + let mut permits = self.permits.lock(); + while *permits == 0 { + self.cvar.wait(&mut permits); + } + *permits -= 1; + drop(permits); + Permit { sem: self.sem.clone() } + } +} + +impl Drop for Permit { + fn drop(&mut self) { + let mut permits = self.sem.permits.lock(); + *permits += 1; + self.sem.cvar.notify_one(); + } +} + /// Locks used for local execution. #[derive(Default)] pub struct ExecutorLocks { transaction: Mutex<()>, + transaction_warmup: Semaphore, } pub struct Executor { @@ -95,7 +143,10 @@ impl Executor { let reject_not_contract = config.executor_reject_not_contract; let evms = EvmWorkerPool::spawn(Arc::clone(&storage), &config); Self { - locks: ExecutorLocks::default(), + locks: ExecutorLocks { + transaction_warmup: Semaphore::new(100), + ..Default::default() + }, evms, miner, storage, @@ -289,6 +340,7 @@ impl Executor { s.rec_str("tx_nonce", &tx.execution_info.nonce); }); + let _permit = self.locks.transaction_warmup.acquire(); if let Some(access_list) = access_list { self.storage.load_access_list(access_list); } @@ -300,11 +352,11 @@ impl Executor { // * Uses a Mutex, so a new transactions starts executing only after the previous one is executed and persisted. // * Without a Mutex, conflict can happen because the next transactions starts executing before the previous one is saved. #[cfg(feature = "metrics")] - let lock_wait_start = metrics::now(); + metrics::inc_executor_local_transaction_lock_waiting(1); let transaction_lock = self.locks.transaction.lock(); - #[cfg(feature = "metrics")] - let lock_wait = lock_wait_start.elapsed(); + metrics::dec_executor_local_transaction_lock_waiting(1); + #[cfg(feature = "metrics")] let start = metrics::now(); @@ -316,8 +368,6 @@ impl Executor { drop(transaction_lock); - #[cfg(feature = "metrics")] - metrics::inc_executor_local_transaction_lock_wait(lock_wait); #[cfg(feature = "metrics")] metrics::inc_executor_local_transaction(execution_elapsed, tx_execution.is_ok(), contract, function); diff --git a/src/infra/metrics/metrics_definitions.rs b/src/infra/metrics/metrics_definitions.rs index 77a912934..0491321bf 100644 --- a/src/infra/metrics/metrics_definitions.rs +++ b/src/infra/metrics/metrics_definitions.rs @@ -135,8 +135,8 @@ metrics! { "Time executing a local transaction." histogram_duration executor_local_transaction{success, contract, function}, - "Time waiting to acquire the local transaction execution lock." - histogram_duration executor_local_transaction_lock_wait{}, + "Number of transactions waiting to acquire the local transaction execution lock." + gauge executor_local_transaction_lock_waiting{}, "Time executing a local transaction." counter executor_local_transaction_reverts{contract, function, reason}, From 9bd902f4d37926ab1ef112e3d7a775a0b7c3c89d Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 10:40:21 -0300 Subject: [PATCH 05/31] transient_state_lock --- src/eth/executor/mod.rs | 4 ++-- src/eth/storage/stratus_storage.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index 6e61a1cdd..bc1f8c137 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -340,6 +340,8 @@ impl Executor { s.rec_str("tx_nonce", &tx.execution_info.nonce); }); + #[cfg(feature = "metrics")] + metrics::inc_executor_local_transaction_lock_waiting(1); let _permit = self.locks.transaction_warmup.acquire(); if let Some(access_list) = access_list { self.storage.load_access_list(access_list); @@ -351,8 +353,6 @@ impl Executor { // Executes transactions serially: // * Uses a Mutex, so a new transactions starts executing only after the previous one is executed and persisted. // * Without a Mutex, conflict can happen because the next transactions starts executing before the previous one is saved. - #[cfg(feature = "metrics")] - metrics::inc_executor_local_transaction_lock_waiting(1); let transaction_lock = self.locks.transaction.lock(); #[cfg(feature = "metrics")] metrics::dec_executor_local_transaction_lock_waiting(1); diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index b385a08bc..2bed676c7 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -820,6 +820,7 @@ impl StratusStorage { } pub fn load_access_list(&self, access_list: AccessListOutput) { + let _guard = self.transient_state_lock.read(); let mut account_addresses = vec![]; let mut slot_keys = vec![]; for (address, slots) in access_list { From 7ffda1da39ea86ced6860ee2d5fe212373672347 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 10:45:45 -0300 Subject: [PATCH 06/31] acquire temp lock only once --- src/eth/storage/stratus_storage.rs | 90 ++++++++++++++++--- .../storage/temporary/inmemory/transaction.rs | 42 +++++---- 2 files changed, 100 insertions(+), 32 deletions(-) diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index 2bed676c7..0ff9e3e5f 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -94,8 +94,11 @@ pub(super) trait EntityRead: Sized + Clone { type Key: Copy; /// Reads the latest (mined tip) value from the cache, if present. fn read_latest_cache(s: &StratusStorage, key: &Self::Key) -> Option; - /// Checks if the latest cache contains the given key. - fn cache_contains_key(s: &StratusStorage, key: &Self::Key) -> bool; + /// Retains only the keys that are missing from both the temporary storage and the latest cache. + /// + /// Batched: the temporary-storage locks are acquired once for the whole key set, instead of + /// once per key, to reduce contention with the executor. + fn retain_missing_keys(s: &StratusStorage, keys: &mut Vec); /// Reads from temporary (pending) storage. fn read_temp(s: &StratusStorage, key: Self::Key) -> Option; /// Reads from permanent storage at the resolved mined point. @@ -125,8 +128,9 @@ impl EntityRead for Account { }) } - fn cache_contains_key(s: &StratusStorage, key: &Self::Key) -> bool { - s.temp.transaction_storage.contains_account(key) || s.cache.contains_account(key) + fn retain_missing_keys(s: &StratusStorage, keys: &mut Vec) { + s.temp.transaction_storage.retain_missing_accounts(keys); + keys.retain(|address| !s.cache.contains_account(address)); } fn read_perm(s: &StratusStorage, address: Address, point: MinedPointInTime<'_>) -> Result { @@ -180,8 +184,9 @@ impl EntityRead for Slot { }) } - fn cache_contains_key(s: &StratusStorage, key: &Self::Key) -> bool { - s.temp.transaction_storage.contains_slot(key) || s.cache.contains_slot(&key.0, &key.1) + fn retain_missing_keys(s: &StratusStorage, keys: &mut Vec) { + s.temp.transaction_storage.retain_missing_slots(keys); + keys.retain(|(address, index)| !s.cache.contains_slot(address, index)); } fn read_perm(s: &StratusStorage, key: (Address, SlotIndex), point: MinedPointInTime<'_>) -> Result { @@ -824,15 +829,13 @@ impl StratusStorage { let mut account_addresses = vec![]; let mut slot_keys = vec![]; for (address, slots) in access_list { - if !Account::cache_contains_key(self, &address) { - account_addresses.push(address); - } + account_addresses.push(address); for slot_index in slots { - if !Slot::cache_contains_key(self, &(address, slot_index)) { - slot_keys.push((address, slot_index)); - } + slot_keys.push((address, slot_index)); } } + Account::retain_missing_keys(self, &mut account_addresses); + Slot::retain_missing_keys(self, &mut slot_keys); self.load_accounts_to_cache(account_addresses); self.load_slots_to_cache(slot_keys); } @@ -846,14 +849,15 @@ mod tests { use crate::eth::executor::TransactionExecutionResult; use crate::eth::executor::types::state::AccountChanges; use crate::eth::executor::types::state::CompleteValue; + use crate::eth::types::Nonce; use crate::eth::types::Signature; use crate::eth::types::SlotValue; use crate::eth::types::TransactionInfo; use crate::eth::types::TransactionInput; use crate::eth::types::Wei; - /// Mines a block applying `changes` - fn mine_block(storage: &StratusStorage, changes: State) -> BlockNumber { + /// Saves an execution applying `changes` to the pending block, without finishing it. + fn save_execution(storage: &StratusStorage, changes: State) { let header = storage.read_pending_block_header(); let evm_input = TransactionExecutionInput::from_eth_transaction(&TransactionInput::default(), header.number, *header.timestamp); @@ -864,6 +868,11 @@ mod tests { let tx = TransactionExecution::new(TransactionInfo::default(), Signature::default(), evm_input, result); storage.save_execution(tx, changes).expect("save execution"); + } + + /// Mines a block applying `changes` + fn mine_block(storage: &StratusStorage, changes: State) -> BlockNumber { + save_execution(storage, changes); let (block, block_changes) = storage.finish_pending_block(); storage.save_block(block.into(), block_changes).expect("save block"); @@ -871,6 +880,59 @@ mod tests { storage.read_mined_block_number() } + /// Keys present in the temporary storage (pending or latest) or in the latest cache must be + /// filtered out by `retain_missing_keys`, keeping only the keys missing from both. + #[test] + fn retain_missing_keys_filters_temporary_and_cached_keys() { + let storage = StratusStorage::new_test().expect("failed to build test storage"); + + let pending_address = Address::new([0xAA; 20]); + let cached_address = Address::new([0xBB; 20]); + let missing_address = Address::new([0xCC; 20]); + + // The cached address is mined and saved, landing in the latest cache. + let mut mined_changes = State::default(); + mined_changes.accounts.insert( + cached_address, + AccountChanges { + nonce: CompleteValue::Changed(Nonce::from(1u64)), + balance: CompleteValue::Changed(Wei::from(1u64)), + bytecode: CompleteValue::Changed(None), + }, + ); + mined_changes + .slots + .insert((cached_address, SlotIndex::ZERO), CompleteValue::Changed(SlotValue::from([200u64, 0, 0, 0]))); + mine_block(&storage, mined_changes); + + // The pending address is saved to the pending block, which is not finished. + let mut pending_changes = State::default(); + pending_changes.accounts.insert( + pending_address, + AccountChanges { + nonce: CompleteValue::Changed(Nonce::from(1u64)), + balance: CompleteValue::Changed(Wei::from(1u64)), + bytecode: CompleteValue::Changed(None), + }, + ); + pending_changes + .slots + .insert((pending_address, SlotIndex::ZERO), CompleteValue::Changed(SlotValue::from([100u64, 0, 0, 0]))); + save_execution(&storage, pending_changes); + + let mut account_addresses = vec![pending_address, cached_address, missing_address]; + Account::retain_missing_keys(&storage, &mut account_addresses); + assert_eq!(account_addresses, vec![missing_address]); + + let mut slot_keys = vec![ + (pending_address, SlotIndex::ZERO), + (cached_address, SlotIndex::ZERO), + (missing_address, SlotIndex::ZERO), + ]; + Slot::retain_missing_keys(&storage, &mut slot_keys); + assert_eq!(slot_keys, vec![(missing_address, SlotIndex::ZERO)]); + } + /// An `eth_call` pinned to a block that is no longer the latest must read the historical /// state at its captured block, not the current latest state. #[test] diff --git a/src/eth/storage/temporary/inmemory/transaction.rs b/src/eth/storage/temporary/inmemory/transaction.rs index d319dd8f1..4269bc96b 100644 --- a/src/eth/storage/temporary/inmemory/transaction.rs +++ b/src/eth/storage/temporary/inmemory/transaction.rs @@ -178,27 +178,33 @@ impl InmemoryTransactionTemporaryStorage { } } - pub fn contains_account(&self, address: &Address) -> bool { - match self.pending_block.read().state.accounts.contains_key(&address) { - true => true, - false => self - .latest_block - .read() - .as_ref() - .map(|latest| latest.state.accounts.contains_key(&address)) - .unwrap_or(false), + /// Retains only the addresses that are missing from both the pending and latest temporary states. + /// + /// Batched: each lock is acquired once for the whole key set, instead of once per key, + /// to reduce contention with the executor. + pub fn retain_missing_accounts(&self, addresses: &mut Vec
) { + { + let pending_block = self.pending_block.read(); + addresses.retain(|address| !pending_block.state.accounts.contains_key(address)); + } + let latest_block = self.latest_block.read(); + if let Some(latest_block) = latest_block.as_ref() { + addresses.retain(|address| !latest_block.state.accounts.contains_key(address)); } } - pub fn contains_slot(&self, slot_key: &(Address, SlotIndex)) -> bool { - match self.pending_block.read().state.slots.contains_key(slot_key) { - true => true, - false => self - .latest_block - .read() - .as_ref() - .map(|latest| latest.state.slots.contains_key(slot_key)) - .unwrap_or(false), + /// Retains only the slot keys that are missing from both the pending and latest temporary states. + /// + /// Batched: each lock is acquired once for the whole key set, instead of once per key, + /// to reduce contention with the executor. + pub fn retain_missing_slots(&self, slot_keys: &mut Vec<(Address, SlotIndex)>) { + { + let pending_block = self.pending_block.read(); + slot_keys.retain(|slot_key| !pending_block.state.slots.contains_key(slot_key)); + } + let latest_block = self.latest_block.read(); + if let Some(latest_block) = latest_block.as_ref() { + slot_keys.retain(|slot_key| !latest_block.state.slots.contains_key(slot_key)); } } From eb0e6114e6523ce3b515fffb17c3fefea6630d0f Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 11:35:35 -0300 Subject: [PATCH 07/31] forward calls on pending to leader to fix bench --- .../follower/importer/importer_supervisor.rs | 20 ++++++++++++++++ .../blockchain_client/blockchain_client.rs | 24 +++++++++++++++++++ src/eth/rpc/server.rs | 11 +++++++++ 3 files changed, 55 insertions(+) diff --git a/src/eth/follower/importer/importer_supervisor.rs b/src/eth/follower/importer/importer_supervisor.rs index 78d1cbbd4..c8b6ce18a 100644 --- a/src/eth/follower/importer/importer_supervisor.rs +++ b/src/eth/follower/importer/importer_supervisor.rs @@ -6,6 +6,7 @@ use anyhow::bail; use futures::try_join; use tokio::sync::mpsc; +use crate::eth::executor::CallExecutionOutput; use crate::eth::executor::Executor; use crate::eth::follower::consensus::Consensus; use crate::eth::follower::consensus::LagDirection; @@ -23,9 +24,13 @@ use crate::eth::follower::importer::importers::fake_leader::FakeLeaderWorker; use crate::eth::follower::importer::importers::replication::ReplicationWorker; use crate::eth::follower::importer::start_number_fetcher; use crate::eth::miner::Miner; +use crate::eth::rpc::BlockFilter; use crate::eth::rpc::BlockchainClient; use crate::eth::storage::StratusStorage; use crate::eth::types::BlockNumber; +use crate::eth::types::CallInput; +use crate::eth::types::Gas; +use crate::eth::types::StratusError; use crate::ext::spawn; use crate::infra::kafka::KafkaConnector; #[cfg(feature = "metrics")] @@ -154,6 +159,21 @@ pub struct ImporterConsensus { pub executor: Arc, } +impl ImporterConsensus { + /// Forwards an `eth_call` to the leader, which executes it against its pending block. + pub async fn forward_call_to_leader(&self, call: CallInput, filter: BlockFilter) -> Result { + tracing::info!(?filter, "forwarding eth_call to leader"); + + let output = self.chain.call_to_leader(call, filter).await?; + + Ok(CallExecutionOutput { + output, + gas_used: Gas::default(), + success: true, + }) + } +} + impl Consensus for ImporterConsensus { async fn lag(&self) -> anyhow::Result { let last_fetched_time = LATEST_FETCHED_BLOCK_TIME.load(Ordering::Relaxed); diff --git a/src/eth/rpc/blockchain_client/blockchain_client.rs b/src/eth/rpc/blockchain_client/blockchain_client.rs index f3e6c9ab5..3aea247df 100644 --- a/src/eth/rpc/blockchain_client/blockchain_client.rs +++ b/src/eth/rpc/blockchain_client/blockchain_client.rs @@ -21,10 +21,13 @@ use crate::alias::AlloyTransaction; use crate::alias::JsonValue; use crate::eth::executor::AccessListOutput; use crate::eth::executor::ExecutorError; +use crate::eth::rpc::BlockFilter; use crate::eth::storage::permanent::rocks::types::BlockChangesRocksdb; use crate::eth::storage::permanent::rocks::types::BlockRocksdb; use crate::eth::types::Address; use crate::eth::types::BlockNumber; +use crate::eth::types::Bytes; +use crate::eth::types::CallInput; use crate::eth::types::ExternalBlock; use crate::eth::types::ExternalBlockWithReceipts; use crate::eth::types::ExternalReceipt; @@ -301,6 +304,27 @@ impl BlockchainClient { } } + /// Forwards an `eth_call` to the leader and returns the executed output. + /// + /// The current machine name is sent as the `x-client` header on every request (see `client_headers`), + /// so the leader attributes the call to this node automatically. + pub async fn call_to_leader(&self, call: CallInput, filter: BlockFilter) -> Result { + tracing::debug!("forwarding eth_call to leader"); + + let call = to_json_value(call); + let filter = to_json_value(filter.to_string()); + let result = self.http.request::("eth_call", [call, filter]).await; + + match result { + Ok(output) => Ok(output), + Err(ClientError::Call(response)) => Err(ExecutorError::LeaderFailed(response.into_owned()).into()), + Err(e) => { + tracing::error!(reason = ?e, "failed to forward eth_call to leader"); + Err(ExecutorError::ForwardToLeaderFailed.into()) + } + } + } + // ------------------------------------------------------------------------- // RPC subscriptions // ------------------------------------------------------------------------- diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index acf88576f..a541108c9 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -1183,6 +1183,17 @@ fn rpc_call(params: Params<'_>, ctx: Arc) -> Result Handle::current().block_on(importer.forward_call_to_leader(call, filter)), + None => { + tracing::error!("unable to forward eth_call because consensus is temporarily unavailable for follower node"); + Err(ConsensusError::Unavailable.into()) + } + }; + } + if let Some(to_address) = call.to && !call.data.is_empty() { From 957e8bc85f7643b9e496d6b20359a6406d626a02 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 11:50:13 -0300 Subject: [PATCH 08/31] drop permit on lock acquisition --- src/eth/executor/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index bc1f8c137..3516a6601 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -342,7 +342,7 @@ impl Executor { #[cfg(feature = "metrics")] metrics::inc_executor_local_transaction_lock_waiting(1); - let _permit = self.locks.transaction_warmup.acquire(); + let permit = self.locks.transaction_warmup.acquire(); if let Some(access_list) = access_list { self.storage.load_access_list(access_list); } @@ -354,6 +354,7 @@ impl Executor { // * Uses a Mutex, so a new transactions starts executing only after the previous one is executed and persisted. // * Without a Mutex, conflict can happen because the next transactions starts executing before the previous one is saved. let transaction_lock = self.locks.transaction.lock(); + drop(permit); #[cfg(feature = "metrics")] metrics::dec_executor_local_transaction_lock_waiting(1); From 47aa54651c35d6e27a45041e3c921293fdbd2acf Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 12:15:29 -0300 Subject: [PATCH 09/31] dont wait for transient sate lock --- src/eth/executor/mod.rs | 16 +++++++++++++--- src/eth/follower/consensus.rs | 2 +- src/eth/rpc/server.rs | 6 +++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index 3516a6601..ae70a99d7 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -462,9 +462,13 @@ impl Executor { } } - /// Executes a transaction without persisting state changes. + /// Executes a read-only call in the local EVM, without persisting state changes. + /// + /// When `skip_transient_lock` is set, storage reads do not acquire the transient state lock. + /// Only meant for access-list computation, where only the set of touched accounts/slots + /// matters and not their values, so the call does not have to wait for a block being saved. #[tracing::instrument(name = "executor::local_call", skip_all, fields(from, to))] - pub fn execute_local_call(&self, call_input: CallInput, point_in_time: PointInTime) -> Result + pub fn execute_local_call(&self, call_input: CallInput, point_in_time: PointInTime, skip_transient_lock: bool) -> Result where Output: TryFrom, { @@ -488,7 +492,7 @@ impl Executor { let (function, contract) = { (codegen::function_sig(&call_input.data), codegen::contract_name(&call_input.to)) }; // execute - let evm_input = match point_in_time { + let mut evm_input = match point_in_time { PointInTime::Pending => { let pending_header = self.storage.read_pending_block_header(); CallExecutionInput::from_pending_block(call_input, pending_header) @@ -501,6 +505,12 @@ impl Executor { } }; + // access-list calls only need the set of touched keys, not their values: use the lock-free + // RPC read kind so they do not wait on the transient state lock held while saving a block + if skip_transient_lock { + evm_input.kind = ExecutionKind::RPC(PointInTime::Latest); + } + let evm_route = match point_in_time { PointInTime::Pending | PointInTime::Latest => EvmRoute::CallPresent(evm_input), PointInTime::Past(_) => EvmRoute::CallPast(evm_input), diff --git a/src/eth/follower/consensus.rs b/src/eth/follower/consensus.rs index 872ac52d4..8638c40f7 100644 --- a/src/eth/follower/consensus.rs +++ b/src/eth/follower/consensus.rs @@ -72,7 +72,7 @@ pub trait Consensus: Send + Sync { let access_list = self .get_executor() - .execute_local_call::(tx.into(), crate::eth::types::PointInTime::Latest)?; + .execute_local_call::(tx.into(), crate::eth::types::PointInTime::Latest, true)?; let hash = self.get_client().send_raw_transaction_to_leader(tx_data.into(), Some(access_list)).await?; diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index a541108c9..9b5bd2053 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -1146,7 +1146,7 @@ fn eth_estimate_gas(params: Params<'_>, ctx: Arc, ext: Extensions) - .executor .validate_to_is_contract(to_address, ExecutionKind::RPC(PointInTime::Latest))?; } - match ctx.server.executor.execute_local_call::(call, PointInTime::Latest) { + match ctx.server.executor.execute_local_call::(call, PointInTime::Latest, false) { // result is success Ok(result) if result.success => { tracing::info!(tx_output = %result.output, "executed eth_estimateGas with success"); @@ -1199,7 +1199,7 @@ fn rpc_call(params: Params<'_>, ctx: Arc) -> Result, ctx: Arc, ext: Extensions) -> Result { @@ -1309,7 +1309,7 @@ fn stratus_access_list(params: Params<'_>, ctx: Arc, ext: Extensions ctx.server .executor - .execute_local_call::(call, PointInTime::Latest) + .execute_local_call::(call, PointInTime::Latest, true) .map(to_json_value) .inspect(|_| tracing::info!("executed stratus_accessList with success")) .inspect_err(|e| tracing::warn!(reason = ?e, "failed to execute stratus_accessList")) From 0eb4a4a8f9fc0c05850398d325010268aceff7b4 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 12:55:44 -0300 Subject: [PATCH 10/31] no need to hold transient state lock --- .../follower/importer/importer_supervisor.rs | 20 ------------------- src/eth/rpc/server.rs | 10 ---------- src/eth/storage/stratus_storage.rs | 1 - 3 files changed, 31 deletions(-) diff --git a/src/eth/follower/importer/importer_supervisor.rs b/src/eth/follower/importer/importer_supervisor.rs index c8b6ce18a..78d1cbbd4 100644 --- a/src/eth/follower/importer/importer_supervisor.rs +++ b/src/eth/follower/importer/importer_supervisor.rs @@ -6,7 +6,6 @@ use anyhow::bail; use futures::try_join; use tokio::sync::mpsc; -use crate::eth::executor::CallExecutionOutput; use crate::eth::executor::Executor; use crate::eth::follower::consensus::Consensus; use crate::eth::follower::consensus::LagDirection; @@ -24,13 +23,9 @@ use crate::eth::follower::importer::importers::fake_leader::FakeLeaderWorker; use crate::eth::follower::importer::importers::replication::ReplicationWorker; use crate::eth::follower::importer::start_number_fetcher; use crate::eth::miner::Miner; -use crate::eth::rpc::BlockFilter; use crate::eth::rpc::BlockchainClient; use crate::eth::storage::StratusStorage; use crate::eth::types::BlockNumber; -use crate::eth::types::CallInput; -use crate::eth::types::Gas; -use crate::eth::types::StratusError; use crate::ext::spawn; use crate::infra::kafka::KafkaConnector; #[cfg(feature = "metrics")] @@ -159,21 +154,6 @@ pub struct ImporterConsensus { pub executor: Arc, } -impl ImporterConsensus { - /// Forwards an `eth_call` to the leader, which executes it against its pending block. - pub async fn forward_call_to_leader(&self, call: CallInput, filter: BlockFilter) -> Result { - tracing::info!(?filter, "forwarding eth_call to leader"); - - let output = self.chain.call_to_leader(call, filter).await?; - - Ok(CallExecutionOutput { - output, - gas_used: Gas::default(), - success: true, - }) - } -} - impl Consensus for ImporterConsensus { async fn lag(&self) -> anyhow::Result { let last_fetched_time = LATEST_FETCHED_BLOCK_TIME.load(Ordering::Relaxed); diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index 9b5bd2053..49a307f20 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -1183,16 +1183,6 @@ fn rpc_call(params: Params<'_>, ctx: Arc) -> Result Handle::current().block_on(importer.forward_call_to_leader(call, filter)), - None => { - tracing::error!("unable to forward eth_call because consensus is temporarily unavailable for follower node"); - Err(ConsensusError::Unavailable.into()) - } - }; - } if let Some(to_address) = call.to && !call.data.is_empty() diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index 0ff9e3e5f..f64153ff8 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -825,7 +825,6 @@ impl StratusStorage { } pub fn load_access_list(&self, access_list: AccessListOutput) { - let _guard = self.transient_state_lock.read(); let mut account_addresses = vec![]; let mut slot_keys = vec![]; for (address, slots) in access_list { From bc9fe3e56bde8d9128d6153e04de38266a796cbf Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 14:57:38 -0300 Subject: [PATCH 11/31] try read cache --- src/eth/storage/cache.rs | 11 +++++++++++ src/eth/storage/resolve_pending.rs | 2 +- src/eth/storage/stratus_storage.rs | 25 +++++++++++++++++++------ src/eth/types/execution_kind.rs | 3 ++- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/eth/storage/cache.rs b/src/eth/storage/cache.rs index 1dfbbefb2..54f5bfc41 100644 --- a/src/eth/storage/cache.rs +++ b/src/eth/storage/cache.rs @@ -7,6 +7,7 @@ use indexmap::Equivalent; use quick_cache::UnitWeighter; use quick_cache::sync::Cache; use quick_cache::sync::GuardResult; +use quick_cache::sync::LockContention; use crate::eth::executor::State; use crate::eth::executor::types::state::Change; @@ -95,6 +96,16 @@ impl StorageCache { .map(|value| Slot { value, index: *index }) } + pub fn try_get_account_latest(&self, address: &Address) -> Result, LockContention> { + self.account_latest_cache.try_get(address) + } + + pub fn try_get_slot_latest(&self, address: &Address, index: &SlotIndex) -> Result, LockContention> { + self.slot_latest_cache + .try_get(&SlotKeyRef(address, index)) + .map(|value| value.map(|value| Slot { value, index: *index })) + } + pub fn contains_account(&self, address: &Address) -> bool { self.account_latest_cache.contains_key(address) } diff --git a/src/eth/storage/resolve_pending.rs b/src/eth/storage/resolve_pending.rs index 1909558fa..bb124dd21 100644 --- a/src/eth/storage/resolve_pending.rs +++ b/src/eth/storage/resolve_pending.rs @@ -106,7 +106,7 @@ impl StratusStorage { match kind { ExecutionKind::RPC(PointInTime::Past(number)) | ExecutionKind::CallPast(number) => MinedPointInTime::past(number), ExecutionKind::CallLatest(block_number) => self.resolve_call_point(block_number), - ExecutionKind::Transaction | ExecutionKind::RPC(_) => MinedPointInTime::latest(None), + ExecutionKind::Transaction | ExecutionKind::RPC(_) | ExecutionKind::AccessList => MinedPointInTime::latest(None), } } } diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index f64153ff8..57e9b31cb 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -94,6 +94,8 @@ pub(super) trait EntityRead: Sized + Clone { type Key: Copy; /// Reads the latest (mined tip) value from the cache, if present. fn read_latest_cache(s: &StratusStorage, key: &Self::Key) -> Option; + /// Tries to read from the latest cache, if it gets a lock contention error from the cache, returns None + fn try_read_latest_cache(s: &StratusStorage, key: &Self::Key) -> Option; /// Retains only the keys that are missing from both the temporary storage and the latest cache. /// /// Batched: the temporary-storage locks are acquired once for the whole key set, instead of @@ -128,6 +130,10 @@ impl EntityRead for Account { }) } + fn try_read_latest_cache(s: &StratusStorage, address: &Address) -> Option { + s.cache.try_get_account_latest(address).ok().flatten() + } + fn retain_missing_keys(s: &StratusStorage, keys: &mut Vec) { s.temp.transaction_storage.retain_missing_accounts(keys); keys.retain(|address| !s.cache.contains_account(address)); @@ -184,6 +190,11 @@ impl EntityRead for Slot { }) } + fn try_read_latest_cache(s: &StratusStorage, key: &Self::Key) -> Option { + let (address, index) = key; + s.cache.try_get_slot_latest(address, index).ok().flatten() + } + fn retain_missing_keys(s: &StratusStorage, keys: &mut Vec) { s.temp.transaction_storage.retain_missing_slots(keys); keys.retain(|(address, index)| !s.cache.contains_slot(address, index)); @@ -382,7 +393,12 @@ impl StratusStorage { MinedPointInTime::Latest(_, _) => // Latest: try latest cache while guard is held, then fall through to perm. { - if let Some(value) = E::read_latest_cache(self, &key) { + let cached_value = if matches!(kind, ExecutionKind::AccessList) { + E::try_read_latest_cache(self, &key) + } else { + E::read_latest_cache(self, &key) + }; + if let Some(value) = cached_value { break 'query (value, FoundAt::Cache); } // If it wasnt found in the cache and we still have the guard the value can only be read in perm latest @@ -397,13 +413,10 @@ impl StratusStorage { // Cache non-historical reads according to the point-in-time and where the value came from. match (kind, found_at) { - (ExecutionKind::Transaction, _) => (), - // A pending read that hit perm (i.e. not in any cache/temp) is already mined, so cache latest. - // OR A mined read that hit perm is the latest state, so populate the latest cache. - (_, FoundAt::PermLatest) => { + // Reads that held the transient state lock and were found at perm can be cached + (ExecutionKind::CallLatest(_) | ExecutionKind::CallPast(_), FoundAt::PermLatest) => { E::cache_latest_if_missing(self, key, value.clone()); } - // Cache / Historical / (Mined, Temp): nothing to cache. _ => {} } Ok(value) diff --git a/src/eth/types/execution_kind.rs b/src/eth/types/execution_kind.rs index fed07edbe..284f88436 100644 --- a/src/eth/types/execution_kind.rs +++ b/src/eth/types/execution_kind.rs @@ -9,6 +9,7 @@ pub enum ExecutionKind { #[default] Transaction, RPC(PointInTime), + AccessList, } impl ExecutionKind { @@ -21,7 +22,7 @@ impl From<&ExecutionKind> for PointInTime { fn from(value: &ExecutionKind) -> Self { match value { ExecutionKind::RPC(pit) => *pit, - ExecutionKind::Transaction => PointInTime::Pending, + ExecutionKind::Transaction | ExecutionKind::AccessList => PointInTime::Pending, ExecutionKind::CallPast(number) => PointInTime::Past(*number), ExecutionKind::CallLatest(_) => PointInTime::Latest, } From 9dfeddd68434bcabd22443a82ff98ea2304de650 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 17:33:03 -0300 Subject: [PATCH 12/31] access list is latest not pending --- src/eth/storage/stratus_storage.rs | 2 +- src/eth/types/execution_kind.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index 57e9b31cb..496ddf7e3 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -384,7 +384,7 @@ impl StratusStorage { } /// Generic read algorithm shared by [`read_account`] and [`read_slot`]. - fn read(&self, key: E::Key, kind: ExecutionKind) -> Result { + fn read(&self, key: E::Key, kind: ExecutionKind) -> Result { let (value, found_at) = 'query: { match E::resolve(self, key, kind) { resolve_pending::Resolved::Temp(value) => break 'query (value, FoundAt::Temp), diff --git a/src/eth/types/execution_kind.rs b/src/eth/types/execution_kind.rs index 284f88436..e04e1b3c6 100644 --- a/src/eth/types/execution_kind.rs +++ b/src/eth/types/execution_kind.rs @@ -22,9 +22,9 @@ impl From<&ExecutionKind> for PointInTime { fn from(value: &ExecutionKind) -> Self { match value { ExecutionKind::RPC(pit) => *pit, - ExecutionKind::Transaction | ExecutionKind::AccessList => PointInTime::Pending, + ExecutionKind::Transaction => PointInTime::Pending, ExecutionKind::CallPast(number) => PointInTime::Past(*number), - ExecutionKind::CallLatest(_) => PointInTime::Latest, + ExecutionKind::CallLatest(_) | ExecutionKind::AccessList => PointInTime::Latest, } } } From c74a24a4b3736d8bf00a45be88c19037bb37a3ec Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 17:40:20 -0300 Subject: [PATCH 13/31] semaphore metrics --- src/eth/executor/mod.rs | 4 +++- src/infra/metrics/metrics_definitions.rs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index ae70a99d7..44040747b 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -340,8 +340,8 @@ impl Executor { s.rec_str("tx_nonce", &tx.execution_info.nonce); }); + metrics::inc_executor_local_transaction_semaphore_waiting(1); #[cfg(feature = "metrics")] - metrics::inc_executor_local_transaction_lock_waiting(1); let permit = self.locks.transaction_warmup.acquire(); if let Some(access_list) = access_list { self.storage.load_access_list(access_list); @@ -353,8 +353,10 @@ impl Executor { // Executes transactions serially: // * Uses a Mutex, so a new transactions starts executing only after the previous one is executed and persisted. // * Without a Mutex, conflict can happen because the next transactions starts executing before the previous one is saved. + metrics::inc_executor_local_transaction_lock_waiting(1); let transaction_lock = self.locks.transaction.lock(); drop(permit); + metrics::dec_executor_local_transaction_semaphore_waiting(1); #[cfg(feature = "metrics")] metrics::dec_executor_local_transaction_lock_waiting(1); diff --git a/src/infra/metrics/metrics_definitions.rs b/src/infra/metrics/metrics_definitions.rs index 0491321bf..6ba82a8c6 100644 --- a/src/infra/metrics/metrics_definitions.rs +++ b/src/infra/metrics/metrics_definitions.rs @@ -138,6 +138,9 @@ metrics! { "Number of transactions waiting to acquire the local transaction execution lock." gauge executor_local_transaction_lock_waiting{}, + "Number of transactions waiting to acquire the local transaction execution lock." + gauge executor_local_transaction_semaphore_waiting{}, + "Time executing a local transaction." counter executor_local_transaction_reverts{contract, function, reason}, From 4f71ba7167255baf5db23a90c0dd2d4354f4e496 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Tue, 1 Sep 2026 21:39:49 -0300 Subject: [PATCH 14/31] use accesslist execution kind --- .../evm/types/input/call_execution.rs | 11 ++----- src/eth/executor/mod.rs | 31 ++++++++++--------- src/eth/follower/consensus.rs | 3 +- src/eth/rpc/server.rs | 21 ++++++++++--- src/eth/storage/stratus_storage.rs | 13 ++++++++ src/eth/types/execution_kind.rs | 7 +++++ 6 files changed, 57 insertions(+), 29 deletions(-) diff --git a/src/eth/executor/evm/types/input/call_execution.rs b/src/eth/executor/evm/types/input/call_execution.rs index 2c1cf002e..3bedc29bc 100644 --- a/src/eth/executor/evm/types/input/call_execution.rs +++ b/src/eth/executor/evm/types/input/call_execution.rs @@ -13,7 +13,6 @@ use crate::eth::types::BlockNumber; use crate::eth::types::Bytes; use crate::eth::types::CallInput; use crate::eth::types::PendingBlockHeader; -use crate::eth::types::PointInTime; use crate::eth::types::UnixTime; use crate::eth::types::Wei; use crate::ext::OptionExt; @@ -62,7 +61,7 @@ pub struct CallExecutionInput { impl CallExecutionInput { /// Creates from a call that was sent directly to Stratus with `eth_call` or `eth_estimateGas` for a pending block. - pub fn from_pending_block(input: CallInput, block: PendingBlockHeader) -> Self { + pub fn from_pending_block(input: CallInput, block: PendingBlockHeader, kind: ExecutionKind) -> Self { Self { from: input.from.unwrap_or(Address::ZERO), to: input.to.map_into(), @@ -70,16 +69,12 @@ impl CallExecutionInput { data: input.data, block_number: block.number, block_timestamp: *block.timestamp, - kind: ExecutionKind::CallLatest(block.number.prev().unwrap_or_default()), + kind, } } /// Creates from a call that was sent directly to Stratus with `eth_call` or `eth_estimateGas` for a mined block. - pub fn from_mined_block(input: CallInput, block: BlockHeader, point_in_time: PointInTime) -> Self { - let kind = match point_in_time { - PointInTime::Latest | PointInTime::Pending => ExecutionKind::CallLatest(block.number), - PointInTime::Past(number) => ExecutionKind::CallPast(number), - }; + pub fn from_mined_block(input: CallInput, block: BlockHeader, kind: ExecutionKind) -> Self { Self { from: input.from.unwrap_or(Address::ZERO), to: input.to.map_into(), diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index 44040747b..c636d8036 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -470,12 +470,13 @@ impl Executor { /// Only meant for access-list computation, where only the set of touched accounts/slots /// matters and not their values, so the call does not have to wait for a block being saved. #[tracing::instrument(name = "executor::local_call", skip_all, fields(from, to))] - pub fn execute_local_call(&self, call_input: CallInput, point_in_time: PointInTime, skip_transient_lock: bool) -> Result + pub fn execute_local_call(&self, call_input: CallInput, kind: ExecutionKind) -> Result where Output: TryFrom, { #[cfg(feature = "metrics")] let start = metrics::now(); + let point_in_time = kind.point_in_time(); Span::with(|s| { s.rec_opt("from", &call_input.from); @@ -494,27 +495,27 @@ impl Executor { let (function, contract) = { (codegen::function_sig(&call_input.data), codegen::contract_name(&call_input.to)) }; // execute - let mut evm_input = match point_in_time { - PointInTime::Pending => { + let evm_input = match kind { + ExecutionKind::Transaction | ExecutionKind::RPC(PointInTime::Pending) | ExecutionKind::AccessList => { let pending_header = self.storage.read_pending_block_header(); - CallExecutionInput::from_pending_block(call_input, pending_header) + CallExecutionInput::from_pending_block(call_input, pending_header, kind) } - _ => { - let Some(block) = self.storage.read_block(point_in_time.into())? else { - return Err(RpcError::BlockFilterInvalid { filter: point_in_time.into() }.into()); + ExecutionKind::CallLatest(block_number) | ExecutionKind::CallPast(block_number) => { + let Some(block) = self.storage.read_block(crate::eth::rpc::BlockFilter::Number(block_number))? else { + return Err(RpcError::BlockFilterInvalid { filter: crate::eth::rpc::BlockFilter::Number(block_number) }.into()); }; - CallExecutionInput::from_mined_block(call_input, block.header, point_in_time) + CallExecutionInput::from_mined_block(call_input, block.header, kind) + }, + ExecutionKind::RPC(pit) => { + let Some(block) = self.storage.read_block(pit.into())? else { + return Err(RpcError::BlockFilterInvalid { filter: pit.into() }.into()); + }; + CallExecutionInput::from_mined_block(call_input, block.header, kind) } }; - // access-list calls only need the set of touched keys, not their values: use the lock-free - // RPC read kind so they do not wait on the transient state lock held while saving a block - if skip_transient_lock { - evm_input.kind = ExecutionKind::RPC(PointInTime::Latest); - } - let evm_route = match point_in_time { - PointInTime::Pending | PointInTime::Latest => EvmRoute::CallPresent(evm_input), + PointInTime::Pending | PointInTime::Latest => EvmRoute::CallPresent(evm_input), // route using execution kind rather than pit PointInTime::Past(_) => EvmRoute::CallPast(evm_input), }; let evm_result = self.evms.execute::(evm_route); diff --git a/src/eth/follower/consensus.rs b/src/eth/follower/consensus.rs index 8638c40f7..b185c4b31 100644 --- a/src/eth/follower/consensus.rs +++ b/src/eth/follower/consensus.rs @@ -6,6 +6,7 @@ use crate::eth::executor::AccessListOutput; use crate::eth::executor::Executor; use crate::eth::rpc::BlockchainClient; use crate::eth::types::Bytes; +use crate::eth::types::ExecutionKind; use crate::eth::types::Hash; use crate::eth::types::StratusError; use crate::eth::types::TransactionInput; @@ -72,7 +73,7 @@ pub trait Consensus: Send + Sync { let access_list = self .get_executor() - .execute_local_call::(tx.into(), crate::eth::types::PointInTime::Latest, true)?; + .execute_local_call::(tx.into(), ExecutionKind::AccessList)?; let hash = self.get_client().send_raw_transaction_to_leader(tx_data.into(), Some(access_list)).await?; diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index 49a307f20..6f367ceca 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -256,7 +256,7 @@ impl Server { async fn health(&self) -> bool { match GlobalState::get_node_mode() { NodeMode::Leader | NodeMode::FakeLeader => true, - NodeMode::Follower => + NodeMode::Follower => { if GlobalState::is_importer_shutdown() { tracing::warn!("stratus is unhealthy because importer is shutdown"); false @@ -268,7 +268,8 @@ impl Server { false } } - }, + } + } } } } @@ -1146,7 +1147,14 @@ fn eth_estimate_gas(params: Params<'_>, ctx: Arc, ext: Extensions) - .executor .validate_to_is_contract(to_address, ExecutionKind::RPC(PointInTime::Latest))?; } - match ctx.server.executor.execute_local_call::(call, PointInTime::Latest, false) { + + let block_number = ctx.server.storage.read_mined_block_number(); + + match ctx + .server + .executor + .execute_local_call::(call, ExecutionKind::call_from_pit(PointInTime::Latest, block_number)) + { // result is success Ok(result) if result.success => { tracing::info!(tx_output = %result.output, "executed eth_estimateGas with success"); @@ -1183,13 +1191,16 @@ fn rpc_call(params: Params<'_>, ctx: Arc) -> Result, ctx: Arc, ext: Extensions) -> Result { @@ -1299,7 +1310,7 @@ fn stratus_access_list(params: Params<'_>, ctx: Arc, ext: Extensions ctx.server .executor - .execute_local_call::(call, PointInTime::Latest, true) + .execute_local_call::(call, ExecutionKind::AccessList) .map(to_json_value) .inspect(|_| tracing::info!("executed stratus_accessList with success")) .inspect_err(|e| tracing::warn!(reason = ?e, "failed to execute stratus_accessList")) diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index 496ddf7e3..1fd55eeaa 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -821,6 +821,19 @@ impl StratusStorage { } } + pub fn translate_to_block_number(&self, block_filter: BlockFilter) -> Result { + match block_filter { + BlockFilter::Pending => Ok(self.read_pending_block_header().number), + BlockFilter::Latest => Ok(self.read_mined_block_number()), + BlockFilter::Hash(_) | BlockFilter::Timestamp(_) => { + let number = self.read_block(block_filter)?.map(|b| b.number()).unwrap_or_default(); // should err + Ok(number) + }, + BlockFilter::Earliest => Ok(BlockNumber::ZERO), + BlockFilter::Number(number) => Ok(number), + } + } + fn load_slots_to_cache(&self, slots: Vec<(Address, SlotIndex)>) { let existing_slots: HashMap<(Address, SlotIndex), SlotValue> = self.perm.read_slots(slots.clone()).unwrap().into_iter().collect(); for (address, index) in slots { diff --git a/src/eth/types/execution_kind.rs b/src/eth/types/execution_kind.rs index e04e1b3c6..b993b3ad2 100644 --- a/src/eth/types/execution_kind.rs +++ b/src/eth/types/execution_kind.rs @@ -16,6 +16,13 @@ impl ExecutionKind { pub fn point_in_time(&self) -> PointInTime { self.into() } + + pub fn call_from_pit(pit: PointInTime, block_number: BlockNumber) -> Self { + match pit { + PointInTime::Latest | PointInTime::Pending => Self::CallLatest(block_number), + PointInTime::Past(number) => Self::CallPast(number) + } + } } impl From<&ExecutionKind> for PointInTime { From 6cb39e627a842d5fce87965e8a1ab57315e7c364 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Wed, 2 Sep 2026 11:01:37 -0300 Subject: [PATCH 15/31] lint --- src/eth/executor/mod.rs | 9 ++++++--- src/eth/rpc/server.rs | 5 ++--- .../storage/permanent/rocks/rocks_permanent.rs | 1 + src/eth/storage/stratus_storage.rs | 16 ++++++++-------- src/eth/types/execution_kind.rs | 2 +- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index c636d8036..b2e6e23f1 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -101,7 +101,7 @@ impl Semaphore { } *permits -= 1; drop(permits); - Permit { sem: self.sem.clone() } + Permit { sem: Arc::clone(&self.sem) } } } @@ -502,10 +502,13 @@ impl Executor { } ExecutionKind::CallLatest(block_number) | ExecutionKind::CallPast(block_number) => { let Some(block) = self.storage.read_block(crate::eth::rpc::BlockFilter::Number(block_number))? else { - return Err(RpcError::BlockFilterInvalid { filter: crate::eth::rpc::BlockFilter::Number(block_number) }.into()); + return Err(RpcError::BlockFilterInvalid { + filter: crate::eth::rpc::BlockFilter::Number(block_number), + } + .into()); }; CallExecutionInput::from_mined_block(call_input, block.header, kind) - }, + } ExecutionKind::RPC(pit) => { let Some(block) = self.storage.read_block(pit.into())? else { return Err(RpcError::BlockFilterInvalid { filter: pit.into() }.into()); diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index 6f367ceca..6518a43ac 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -256,7 +256,7 @@ impl Server { async fn health(&self) -> bool { match GlobalState::get_node_mode() { NodeMode::Leader | NodeMode::FakeLeader => true, - NodeMode::Follower => { + NodeMode::Follower => if GlobalState::is_importer_shutdown() { tracing::warn!("stratus is unhealthy because importer is shutdown"); false @@ -268,8 +268,7 @@ impl Server { false } } - } - } + }, } } } diff --git a/src/eth/storage/permanent/rocks/rocks_permanent.rs b/src/eth/storage/permanent/rocks/rocks_permanent.rs index 7aa67d5e0..133158478 100644 --- a/src/eth/storage/permanent/rocks/rocks_permanent.rs +++ b/src/eth/storage/permanent/rocks/rocks_permanent.rs @@ -178,6 +178,7 @@ impl RocksPermanentStorage { }) } + #[allow(clippy::type_complexity)] pub fn read_slots(&self, slot_keys: Vec<(Address, SlotIndex)>) -> anyhow::Result, StorageError> { self.state.read_slots(slot_keys).map_err(|err| StorageError::RocksError { err }) } diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index 1fd55eeaa..8056ddf54 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -411,14 +411,14 @@ impl StratusStorage { } }; - // Cache non-historical reads according to the point-in-time and where the value came from. - match (kind, found_at) { - // Reads that held the transient state lock and were found at perm can be cached - (ExecutionKind::CallLatest(_) | ExecutionKind::CallPast(_), FoundAt::PermLatest) => { - E::cache_latest_if_missing(self, key, value.clone()); - } - _ => {} + // Reads that held the transient state lock and were found at perm can be cached + if matches!( + (kind, found_at), + (ExecutionKind::CallLatest(_) | ExecutionKind::CallPast(_), FoundAt::PermLatest) + ) { + E::cache_latest_if_missing(self, key, value.clone()); } + Ok(value) } @@ -828,7 +828,7 @@ impl StratusStorage { BlockFilter::Hash(_) | BlockFilter::Timestamp(_) => { let number = self.read_block(block_filter)?.map(|b| b.number()).unwrap_or_default(); // should err Ok(number) - }, + } BlockFilter::Earliest => Ok(BlockNumber::ZERO), BlockFilter::Number(number) => Ok(number), } diff --git a/src/eth/types/execution_kind.rs b/src/eth/types/execution_kind.rs index b993b3ad2..61d417cd3 100644 --- a/src/eth/types/execution_kind.rs +++ b/src/eth/types/execution_kind.rs @@ -20,7 +20,7 @@ impl ExecutionKind { pub fn call_from_pit(pit: PointInTime, block_number: BlockNumber) -> Self { match pit { PointInTime::Latest | PointInTime::Pending => Self::CallLatest(block_number), - PointInTime::Past(number) => Self::CallPast(number) + PointInTime::Past(number) => Self::CallPast(number), } } } From 29f17ba27e6cee47d2c6cd4af98b9f669df8fcad Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Wed, 2 Sep 2026 12:27:24 -0300 Subject: [PATCH 16/31] reorder drops/metrics --- src/eth/executor/mod.rs | 69 ++++++----------------------------------- 1 file changed, 10 insertions(+), 59 deletions(-) diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index b2e6e23f1..2f4f4fbe7 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -12,7 +12,6 @@ use alloy_rpc_types_trace::geth::GethDebugTracingOptions; use alloy_rpc_types_trace::geth::GethTrace; use anyhow::bail; pub use config::ExecutorConfig; -use derive_more::Deref; pub use evm::types::AccessListOutput; pub use evm::types::CallExecutionOutput; pub use evm::types::EvmExecutionMetrics; @@ -20,7 +19,6 @@ pub use evm::types::EvmKind; pub use evm::types::TransactionExecutionInput; pub use evm::types::TransactionExecutionOutput; pub use evm::types::TransactionExecutionResult; -use parking_lot::Condvar; use parking_lot::Mutex; use tracing::Span; use tracing::debug_span; @@ -63,56 +61,12 @@ use crate::ext::to_json_string; use crate::infra::metrics; use crate::infra::metrics::timed; use crate::infra::tracing::SpanExt; +use crate::utils::Semaphore; // ----------------------------------------------------------------------------- // Executor // ----------------------------------------------------------------------------- -#[derive(Deref, Default)] -struct Semaphore { - #[deref] - sem: Arc, -} - -#[derive(Default)] -struct SemaphoreInner { - permits: Mutex, - cvar: Condvar, -} - -struct Permit { - sem: Arc, -} - -impl Semaphore { - fn new(permits: usize) -> Self { - Self { - sem: Arc::new(SemaphoreInner { - permits: Mutex::new(permits), - cvar: Condvar::new(), - }), - } - } - - fn acquire(&self) -> Permit { - let mut permits = self.permits.lock(); - while *permits == 0 { - self.cvar.wait(&mut permits); - } - *permits -= 1; - drop(permits); - Permit { sem: Arc::clone(&self.sem) } - } -} - -impl Drop for Permit { - fn drop(&mut self) { - let mut permits = self.sem.permits.lock(); - *permits += 1; - self.sem.cvar.notify_one(); - } -} - /// Locks used for local execution. #[derive(Default)] pub struct ExecutorLocks { @@ -339,10 +293,12 @@ impl Executor { s.rec_opt("tx_to", &tx.execution_info.to); s.rec_str("tx_nonce", &tx.execution_info.nonce); }); - - metrics::inc_executor_local_transaction_semaphore_waiting(1); #[cfg(feature = "metrics")] + metrics::inc_executor_local_transaction_semaphore_waiting(1); let permit = self.locks.transaction_warmup.acquire(); + #[cfg(feature = "metrics")] + metrics::dec_executor_local_transaction_semaphore_waiting(1); + if let Some(access_list) = access_list { self.storage.load_access_list(access_list); } @@ -353,15 +309,13 @@ impl Executor { // Executes transactions serially: // * Uses a Mutex, so a new transactions starts executing only after the previous one is executed and persisted. // * Without a Mutex, conflict can happen because the next transactions starts executing before the previous one is saved. + #[cfg(feature = "metrics")] metrics::inc_executor_local_transaction_lock_waiting(1); let transaction_lock = self.locks.transaction.lock(); - drop(permit); - metrics::dec_executor_local_transaction_semaphore_waiting(1); - #[cfg(feature = "metrics")] - metrics::dec_executor_local_transaction_lock_waiting(1); + let start = metrics::now(); #[cfg(feature = "metrics")] - let start = metrics::now(); + metrics::dec_executor_local_transaction_lock_waiting(1); // execute transaction let tx_execution = self.execute_local_transaction_attempts(tx, INFINITE_ATTEMPTS); @@ -370,6 +324,7 @@ impl Executor { let execution_elapsed = start.elapsed(); drop(transaction_lock); + drop(permit); #[cfg(feature = "metrics")] metrics::inc_executor_local_transaction(execution_elapsed, tx_execution.is_ok(), contract, function); @@ -465,10 +420,6 @@ impl Executor { } /// Executes a read-only call in the local EVM, without persisting state changes. - /// - /// When `skip_transient_lock` is set, storage reads do not acquire the transient state lock. - /// Only meant for access-list computation, where only the set of touched accounts/slots - /// matters and not their values, so the call does not have to wait for a block being saved. #[tracing::instrument(name = "executor::local_call", skip_all, fields(from, to))] pub fn execute_local_call(&self, call_input: CallInput, kind: ExecutionKind) -> Result where @@ -518,7 +469,7 @@ impl Executor { }; let evm_route = match point_in_time { - PointInTime::Pending | PointInTime::Latest => EvmRoute::CallPresent(evm_input), // route using execution kind rather than pit + PointInTime::Pending | PointInTime::Latest => EvmRoute::CallPresent(evm_input), // // route using execution kind rather than pit PointInTime::Past(_) => EvmRoute::CallPast(evm_input), }; let evm_result = self.evms.execute::(evm_route); From 1831ac723cb4e57b990ffa8a1012d0d099a73bee Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Wed, 2 Sep 2026 12:27:49 -0300 Subject: [PATCH 17/31] remove unused func --- .../blockchain_client/blockchain_client.rs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/eth/rpc/blockchain_client/blockchain_client.rs b/src/eth/rpc/blockchain_client/blockchain_client.rs index 3aea247df..f3e6c9ab5 100644 --- a/src/eth/rpc/blockchain_client/blockchain_client.rs +++ b/src/eth/rpc/blockchain_client/blockchain_client.rs @@ -21,13 +21,10 @@ use crate::alias::AlloyTransaction; use crate::alias::JsonValue; use crate::eth::executor::AccessListOutput; use crate::eth::executor::ExecutorError; -use crate::eth::rpc::BlockFilter; use crate::eth::storage::permanent::rocks::types::BlockChangesRocksdb; use crate::eth::storage::permanent::rocks::types::BlockRocksdb; use crate::eth::types::Address; use crate::eth::types::BlockNumber; -use crate::eth::types::Bytes; -use crate::eth::types::CallInput; use crate::eth::types::ExternalBlock; use crate::eth::types::ExternalBlockWithReceipts; use crate::eth::types::ExternalReceipt; @@ -304,27 +301,6 @@ impl BlockchainClient { } } - /// Forwards an `eth_call` to the leader and returns the executed output. - /// - /// The current machine name is sent as the `x-client` header on every request (see `client_headers`), - /// so the leader attributes the call to this node automatically. - pub async fn call_to_leader(&self, call: CallInput, filter: BlockFilter) -> Result { - tracing::debug!("forwarding eth_call to leader"); - - let call = to_json_value(call); - let filter = to_json_value(filter.to_string()); - let result = self.http.request::("eth_call", [call, filter]).await; - - match result { - Ok(output) => Ok(output), - Err(ClientError::Call(response)) => Err(ExecutorError::LeaderFailed(response.into_owned()).into()), - Err(e) => { - tracing::error!(reason = ?e, "failed to forward eth_call to leader"); - Err(ExecutorError::ForwardToLeaderFailed.into()) - } - } - } - // ------------------------------------------------------------------------- // RPC subscriptions // ------------------------------------------------------------------------- From 5394302e1810beb9b61660a4884af4ed68ddf06c Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Wed, 2 Sep 2026 12:32:00 -0300 Subject: [PATCH 18/31] improve translations --- src/eth/storage/stratus_storage.rs | 81 +++++------------------------- 1 file changed, 13 insertions(+), 68 deletions(-) diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index 8056ddf54..3ebedc743 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -97,9 +97,6 @@ pub(super) trait EntityRead: Sized + Clone { /// Tries to read from the latest cache, if it gets a lock contention error from the cache, returns None fn try_read_latest_cache(s: &StratusStorage, key: &Self::Key) -> Option; /// Retains only the keys that are missing from both the temporary storage and the latest cache. - /// - /// Batched: the temporary-storage locks are acquired once for the whole key set, instead of - /// once per key, to reduce contention with the executor. fn retain_missing_keys(s: &StratusStorage, keys: &mut Vec); /// Reads from temporary (pending) storage. fn read_temp(s: &StratusStorage, key: Self::Key) -> Option; @@ -394,6 +391,7 @@ impl StratusStorage { // Latest: try latest cache while guard is held, then fall through to perm. { let cached_value = if matches!(kind, ExecutionKind::AccessList) { + //bench without try_read E::try_read_latest_cache(self, &key) } else { E::read_latest_cache(self, &key) @@ -814,10 +812,10 @@ impl StratusStorage { BlockFilter::Latest => Ok(PointInTime::Latest), BlockFilter::Earliest => Ok(PointInTime::Past(BlockNumber::ZERO)), BlockFilter::Number(number) => Ok(PointInTime::Past(number)), - BlockFilter::Hash(_) | BlockFilter::Timestamp(_) => match self.read_block(block_filter)? { - Some(block) => Ok(PointInTime::Past(block.header.number)), - None => Err(StorageError::BlockNotFound { filter: block_filter }), - }, + BlockFilter::Hash(_) | BlockFilter::Timestamp(_) => self + .read_block(block_filter)? + .map(|b| PointInTime::Past(b.number())) + .ok_or(StorageError::BlockNotFound { filter: block_filter }), } } @@ -825,17 +823,17 @@ impl StratusStorage { match block_filter { BlockFilter::Pending => Ok(self.read_pending_block_header().number), BlockFilter::Latest => Ok(self.read_mined_block_number()), - BlockFilter::Hash(_) | BlockFilter::Timestamp(_) => { - let number = self.read_block(block_filter)?.map(|b| b.number()).unwrap_or_default(); // should err - Ok(number) - } + BlockFilter::Hash(_) | BlockFilter::Timestamp(_) => self + .read_block(block_filter)? + .map(|b| b.number()) + .ok_or(StorageError::BlockNotFound { filter: block_filter }), BlockFilter::Earliest => Ok(BlockNumber::ZERO), BlockFilter::Number(number) => Ok(number), } } fn load_slots_to_cache(&self, slots: Vec<(Address, SlotIndex)>) { - let existing_slots: HashMap<(Address, SlotIndex), SlotValue> = self.perm.read_slots(slots.clone()).unwrap().into_iter().collect(); + let existing_slots: HashMap<(Address, SlotIndex), SlotValue> = self.perm.read_slots(slots.clone()).unwrap().into_iter().collect(); //unwrap for (address, index) in slots { let value = existing_slots.get(&(address, index)).copied().unwrap_or_default(); Slot::cache_latest_if_missing(self, (address, index), Slot { index, value }); @@ -843,7 +841,7 @@ impl StratusStorage { } fn load_accounts_to_cache(&self, addresses: Vec
) { - let existing_accounts: HashMap = self.perm.read_accounts(addresses.clone()).unwrap().into_iter().collect(); + let existing_accounts: HashMap = self.perm.read_accounts(addresses.clone()).unwrap().into_iter().collect(); //unwrap for address in addresses { let account = existing_accounts.get(&address).cloned().unwrap_or_default(); Account::cache_latest_if_missing(self, address, account); @@ -851,6 +849,7 @@ impl StratusStorage { } pub fn load_access_list(&self, access_list: AccessListOutput) { + // can error let mut account_addresses = vec![]; let mut slot_keys = vec![]; for (address, slots) in access_list { @@ -858,7 +857,7 @@ impl StratusStorage { for slot_index in slots { slot_keys.push((address, slot_index)); } - } + } // skip each step if prev empty Account::retain_missing_keys(self, &mut account_addresses); Slot::retain_missing_keys(self, &mut slot_keys); self.load_accounts_to_cache(account_addresses); @@ -874,7 +873,6 @@ mod tests { use crate::eth::executor::TransactionExecutionResult; use crate::eth::executor::types::state::AccountChanges; use crate::eth::executor::types::state::CompleteValue; - use crate::eth::types::Nonce; use crate::eth::types::Signature; use crate::eth::types::SlotValue; use crate::eth::types::TransactionInfo; @@ -905,59 +903,6 @@ mod tests { storage.read_mined_block_number() } - /// Keys present in the temporary storage (pending or latest) or in the latest cache must be - /// filtered out by `retain_missing_keys`, keeping only the keys missing from both. - #[test] - fn retain_missing_keys_filters_temporary_and_cached_keys() { - let storage = StratusStorage::new_test().expect("failed to build test storage"); - - let pending_address = Address::new([0xAA; 20]); - let cached_address = Address::new([0xBB; 20]); - let missing_address = Address::new([0xCC; 20]); - - // The cached address is mined and saved, landing in the latest cache. - let mut mined_changes = State::default(); - mined_changes.accounts.insert( - cached_address, - AccountChanges { - nonce: CompleteValue::Changed(Nonce::from(1u64)), - balance: CompleteValue::Changed(Wei::from(1u64)), - bytecode: CompleteValue::Changed(None), - }, - ); - mined_changes - .slots - .insert((cached_address, SlotIndex::ZERO), CompleteValue::Changed(SlotValue::from([200u64, 0, 0, 0]))); - mine_block(&storage, mined_changes); - - // The pending address is saved to the pending block, which is not finished. - let mut pending_changes = State::default(); - pending_changes.accounts.insert( - pending_address, - AccountChanges { - nonce: CompleteValue::Changed(Nonce::from(1u64)), - balance: CompleteValue::Changed(Wei::from(1u64)), - bytecode: CompleteValue::Changed(None), - }, - ); - pending_changes - .slots - .insert((pending_address, SlotIndex::ZERO), CompleteValue::Changed(SlotValue::from([100u64, 0, 0, 0]))); - save_execution(&storage, pending_changes); - - let mut account_addresses = vec![pending_address, cached_address, missing_address]; - Account::retain_missing_keys(&storage, &mut account_addresses); - assert_eq!(account_addresses, vec![missing_address]); - - let mut slot_keys = vec![ - (pending_address, SlotIndex::ZERO), - (cached_address, SlotIndex::ZERO), - (missing_address, SlotIndex::ZERO), - ]; - Slot::retain_missing_keys(&storage, &mut slot_keys); - assert_eq!(slot_keys, vec![(missing_address, SlotIndex::ZERO)]); - } - /// An `eth_call` pinned to a block that is no longer the latest must read the historical /// state at its captured block, not the current latest state. #[test] From fda986be6effd273ba3286455ef437f3708cbf3a Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Wed, 2 Sep 2026 12:32:34 -0300 Subject: [PATCH 19/31] fix comment --- src/eth/storage/cache.rs | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/src/eth/storage/cache.rs b/src/eth/storage/cache.rs index 54f5bfc41..3f411a3e1 100644 --- a/src/eth/storage/cache.rs +++ b/src/eth/storage/cache.rs @@ -115,10 +115,7 @@ impl StorageCache { } } -// Borrowed lookup key for `slot_latest_cache`. `std` provides no `Borrow<(&A, &B)>` impl for `(A, B)`, so a tuple of -// references cannot be used directly with `get`/`contains_key`; and the orphan rule (E0117) forbids implementing -// `Equivalent` for `(&Address, &SlotIndex)` directly because tuples are always foreign. The derived `Hash` hashes the -// fields in the same order as the `(Address, SlotIndex)` tuple, which is required for the lookups to match. +/// Borrowed lookup key for `slot_latest_cache`. #[derive(Hash)] struct SlotKeyRef<'a>(&'a Address, &'a SlotIndex); @@ -154,36 +151,3 @@ where } } } - -#[cfg(test)] -mod tests { - use super::*; - - fn new_cache() -> StorageCache { - CacheConfig { - account_history_cache_capacity: 16, - slot_history_cache_capacity: 16, - } - .init() - } - - #[test] - fn contains_slot_finds_cached_slots() { - let cache = new_cache(); - let address = Address::new([0xAA; 20]); - let index = SlotIndex::from([7u64, 0, 0, 0]); - let other_index = SlotIndex::from([8u64, 0, 0, 0]); - let other_address = Address::new([0xBB; 20]); - - cache.cache_slot_latest_if_missing(address, Slot::new(index, SlotValue::from([42u64, 0, 0, 0]))); - - let expected_slot = Slot::new(index, SlotValue::from([42u64, 0, 0, 0])); - assert_eq!(cache.get_slot_latest(&address, &index), Some(expected_slot)); - assert_eq!(cache.get_slot_latest(&address, &other_index), None); - assert_eq!(cache.get_slot_latest(&other_address, &index), None); - - assert!(cache.contains_slot(&address, &index)); - assert!(!cache.contains_slot(&address, &other_index)); - assert!(!cache.contains_slot(&other_address, &index)); - } -} From 4252b3bc24f19612b530815c5afcf1b14c75c4ab Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Wed, 2 Sep 2026 12:36:13 -0300 Subject: [PATCH 20/31] refac multiget funcs --- .../storage/permanent/rocks/rocks_state.rs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/eth/storage/permanent/rocks/rocks_state.rs b/src/eth/storage/permanent/rocks/rocks_state.rs index 9cbdd5abb..d2a8ea833 100644 --- a/src/eth/storage/permanent/rocks/rocks_state.rs +++ b/src/eth/storage/permanent/rocks/rocks_state.rs @@ -349,13 +349,12 @@ impl RocksStorageState { } pub fn read_slots(&self, slot_keys: Vec<(Address, SlotIndex)>) -> Result> { - self.account_slots - .multi_get(slot_keys.into_iter().map(|(address, index)| (address.into(), index.into()))) - .map(|vec| { - vec.into_iter() - .map(|((address, index), slot_value)| ((address.into(), index.into()), slot_value.into_inner().into())) - .collect_vec() - }) + Ok(self + .account_slots + .multi_get(slot_keys.into_iter().map(|(address, index)| (address.into(), index.into())))? + .into_iter() + .map(|((address, index), slot_value)| ((address.into(), index.into()), slot_value.into_inner().into())) + .collect_vec()) } pub fn read_account(&self, address: Address, point: &MinedPointInTime<'_>) -> Result> { @@ -392,9 +391,12 @@ impl RocksStorageState { } pub fn read_accounts(&self, addresses: Vec
) -> Result> { - self.accounts - .multi_get(addresses.into_iter().map_into()) - .map(|vec| vec.into_iter().map(|(addr, acc)| (addr.into(), acc.to_account(addr.into()))).collect_vec()) + Ok(self + .accounts + .multi_get(addresses.into_iter().map_into())? + .into_iter() + .map(|(addr, acc)| (addr.into(), acc.to_account(addr.into()))) + .collect_vec()) } pub fn read_block(&self, selection: BlockFilter) -> Result> { @@ -411,12 +413,13 @@ impl RocksStorageState { BlockFilter::Latest | BlockFilter::Pending => self.blocks_by_number.last_value(), BlockFilter::Earliest => self.blocks_by_number.first_value(), BlockFilter::Number(block_number) => self.blocks_by_number.get(&block_number.into()), - BlockFilter::Hash(block_hash) => + BlockFilter::Hash(block_hash) => { if let Some(block_number) = self.blocks_by_hash.get(&block_hash.into())? { self.blocks_by_number.get(&block_number) } else { Ok(None) - }, + } + } BlockFilter::Timestamp(timestamp) => self .blocks_by_timestamp .iter_from(timestamp.timestamp.into(), timestamp.mode.into())? From 2e7b36faf9ca298bb50dd55cc55d2aa08ae7afda Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Wed, 2 Sep 2026 12:39:24 -0300 Subject: [PATCH 21/31] metrify semaphore queue in acquire() --- src/eth/executor/mod.rs | 5 +--- src/utils.rs | 57 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index 2f4f4fbe7..27ce78ba2 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -293,11 +293,8 @@ impl Executor { s.rec_opt("tx_to", &tx.execution_info.to); s.rec_str("tx_nonce", &tx.execution_info.nonce); }); - #[cfg(feature = "metrics")] - metrics::inc_executor_local_transaction_semaphore_waiting(1); + let permit = self.locks.transaction_warmup.acquire(); - #[cfg(feature = "metrics")] - metrics::dec_executor_local_transaction_semaphore_waiting(1); if let Some(access_list) = access_list { self.storage.load_access_list(access_list); diff --git a/src/utils.rs b/src/utils.rs index b01ec3c6e..46535cdfd 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,14 @@ +use std::sync::Arc; use std::time::Duration; +use derive_more::Deref; +use parking_lot::Condvar; +use parking_lot::Mutex; use tokio::time::Instant; +#[cfg(feature = "metrics")] +use crate::infra::metrics; + /// Amount of bytes in one GB (technically, GiB). pub const GIGABYTE: usize = 1024 * 1024 * 1024; @@ -107,3 +114,53 @@ pub mod test_utils { Uint::random_with(&mut rng) } } + +#[derive(Deref, Default)] +pub struct Semaphore { + // refac to another file + #[deref] + sem: Arc, +} + +#[derive(Default)] +pub struct SemaphoreInner { + permits: Mutex, + cvar: Condvar, +} + +pub struct Permit { + sem: Arc, +} + +impl Semaphore { + pub fn new(permits: usize) -> Self { + Self { + sem: Arc::new(SemaphoreInner { + permits: Mutex::new(permits), + cvar: Condvar::new(), + }), + } + } + + pub fn acquire(&self) -> Permit { + #[cfg(feature = "metrics")] + metrics::inc_executor_local_transaction_semaphore_waiting(1); + let mut permits = self.permits.lock(); + while *permits == 0 { + self.cvar.wait(&mut permits); + } + *permits -= 1; + drop(permits); + #[cfg(feature = "metrics")] + metrics::dec_executor_local_transaction_semaphore_waiting(1); + Permit { sem: Arc::clone(&self.sem) } + } +} + +impl Drop for Permit { + fn drop(&mut self) { + let mut permits = self.sem.permits.lock(); + *permits += 1; + self.sem.cvar.notify_one(); + } +} From 28b3e10695d5302f8d07b4d8d633039eba7c3bec Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Wed, 2 Sep 2026 12:43:28 -0300 Subject: [PATCH 22/31] fmt --- src/eth/storage/permanent/rocks/rocks_state.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/eth/storage/permanent/rocks/rocks_state.rs b/src/eth/storage/permanent/rocks/rocks_state.rs index d2a8ea833..c6fd70e50 100644 --- a/src/eth/storage/permanent/rocks/rocks_state.rs +++ b/src/eth/storage/permanent/rocks/rocks_state.rs @@ -413,13 +413,12 @@ impl RocksStorageState { BlockFilter::Latest | BlockFilter::Pending => self.blocks_by_number.last_value(), BlockFilter::Earliest => self.blocks_by_number.first_value(), BlockFilter::Number(block_number) => self.blocks_by_number.get(&block_number.into()), - BlockFilter::Hash(block_hash) => { + BlockFilter::Hash(block_hash) => if let Some(block_number) = self.blocks_by_hash.get(&block_hash.into())? { self.blocks_by_number.get(&block_number) } else { Ok(None) - } - } + }, BlockFilter::Timestamp(timestamp) => self .blocks_by_timestamp .iter_from(timestamp.timestamp.into(), timestamp.mode.into())? From 2e9385a75a576f152874b66e8e5c4fc803b96db6 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Wed, 2 Sep 2026 15:27:28 -0300 Subject: [PATCH 23/31] refac sendrawtransaction request parsing out of middleware --- src/eth/follower/consensus.rs | 2 +- src/eth/rpc/context.rs | 4 + src/eth/rpc/middleware/mod.rs | 1 + src/eth/rpc/middleware/rpc_middleware.rs | 167 ++++++++++------------- src/eth/rpc/mod.rs | 1 - src/eth/rpc/server.rs | 82 ++++++++--- 6 files changed, 141 insertions(+), 116 deletions(-) diff --git a/src/eth/follower/consensus.rs b/src/eth/follower/consensus.rs index b185c4b31..bbb20dd00 100644 --- a/src/eth/follower/consensus.rs +++ b/src/eth/follower/consensus.rs @@ -71,7 +71,7 @@ pub trait Consensus: Send + Sync { tracing::info!(%tx_hash, "forwarding transaction to leader"); - let access_list = self + let access_list = self // make this configurable (?) .get_executor() .execute_local_call::(tx.into(), ExecutionKind::AccessList)?; diff --git a/src/eth/rpc/context.rs b/src/eth/rpc/context.rs index 0a78d13e3..0c072be1e 100644 --- a/src/eth/rpc/context.rs +++ b/src/eth/rpc/context.rs @@ -1,9 +1,13 @@ use std::sync::Arc; +use derive_more::Debug; + use super::Server; use crate::eth::rpc::subscriptions::RpcSubscriptionsConnected; +#[derive(Debug)] pub struct RpcContext { + #[debug(skip)] pub server: Arc, pub client_version: &'static str, pub subs: Arc, diff --git a/src/eth/rpc/middleware/mod.rs b/src/eth/rpc/middleware/mod.rs index 93a02c0e8..b06835baf 100644 --- a/src/eth/rpc/middleware/mod.rs +++ b/src/eth/rpc/middleware/mod.rs @@ -7,3 +7,4 @@ pub use decode::decode_input_arguments; pub use http_middleware::Authentication; pub use http_middleware::RpcHttpMiddleware; pub use rpc_middleware::RpcMiddleware; +pub use rpc_middleware::TransactionTracingIdentifiers; diff --git a/src/eth/rpc/middleware/rpc_middleware.rs b/src/eth/rpc/middleware/rpc_middleware.rs index 9c0795bd3..993739a9a 100644 --- a/src/eth/rpc/middleware/rpc_middleware.rs +++ b/src/eth/rpc/middleware/rpc_middleware.rs @@ -29,15 +29,14 @@ use crate::alias::JsonValue; use crate::eth::codegen; use crate::eth::codegen::ContractName; use crate::eth::codegen::SoliditySignature; -use crate::eth::executor::AccessListOutput; use crate::eth::rpc::RpcClientApp; +use crate::eth::rpc::RpcContext; use crate::eth::rpc::RpcError; use crate::eth::rpc::middleware::multicall::MulticallInfo; use crate::eth::rpc::next_rpc_param; -use crate::eth::rpc::parse_rpc_rlp; use crate::eth::rpc::parser::RpcExtensionsExt; +use crate::eth::rpc::server::eth_send_raw_transaction; use crate::eth::types::Address; -use crate::eth::types::Bytes; use crate::eth::types::CallInput; #[cfg(feature = "metrics")] use crate::eth::types::ErrorCode; @@ -62,11 +61,15 @@ use crate::infra::tracing::new_cid; #[derive(Debug, Clone)] pub struct RpcMiddleware { service: Arc, + ctx: Arc, } impl RpcMiddleware { - pub fn new(service: RpcService) -> Self { - Self { service: Arc::new(service) } + pub fn new(service: RpcService, ctx: Arc) -> Self { + Self { + service: Arc::new(service), + ctx, + } } } @@ -163,13 +166,24 @@ impl RpcServiceT for RpcMiddleware { fn call<'a>(&self, mut request: jsonrpsee::types::Request<'a>) -> impl Future + Send + 'a { let request_type = request.extensions().get::().copied().unwrap_or_default(); + let is_admin = request.extensions.is_admin(); + let client = request.extensions.rpc_client().to_owned(); + let request_id = request.id(); + let method = request.method_name().to_owned(); + let request_params_str = to_json_string(&request.params); + #[cfg(feature = "metrics")] + if let Some(guard) = request.extensions.get::() { + let active = guard.max_connections() - guard.available_connections(); + metrics::set_rpc_requests_active(active as u64); + } + let span = info_span!( parent: None, "rpc::request", cid = %new_cid(), - rpc_client = field::Empty, - rpc_id = field::Empty, - rpc_method = field::Empty, + rpc_client = %client, + rpc_id = %request_id, + rpc_method = %method, rpc_tx_hash = field::Empty, rpc_tx_from = field::Empty, rpc_tx_to = field::Empty, @@ -182,67 +196,55 @@ impl RpcServiceT for RpcMiddleware { ); let middleware_enter = span.enter(); - // extract request data - let method = request.method_name().to_owned(); - let mut tx = None; - - let params_clone = request.params().clone(); - - if method == "eth_sendRawTransaction" { - let tx_data_result = next_rpc_param::(params_clone.sequence()); - - if let Ok((next_param, tx_data)) = tx_data_result { - let access_list = next_rpc_param::>(next_param) - .map(|(_params, access_list)| access_list) - .inspect_err(|err| tracing::warn!(?err, "failed to deserialize access list")) - .ok() - .flatten(); - let decoded_tx_result = parse_rpc_rlp::(&tx_data); - - if let Ok(decoded_tx) = decoded_tx_result { - tx = TransactionTracingIdentifiers::from_raw_transaction(&decoded_tx).ok(); + // trace event + Span::with(|s| { + s.rec_str("rpc_id", &request_id); + s.rec_str("rpc_client", &client); + s.rec_str("rpc_method", &method); + }); - request.extensions_mut().insert(tx_data); - request.extensions_mut().insert(decoded_tx); - request.extensions_mut().insert(access_list); - } - } + let (future, tracing_identifiers) = if method == "eth_sendRawTransaction" { + drop(middleware_enter); + eth_send_raw_transaction(request, Arc::clone(&self.ctx), span).unwrap() } else { - tx = match method.as_str() { - "eth_call" | "eth_estimateGas" => TransactionTracingIdentifiers::from_call(params_clone.clone()).ok(), - "eth_getTransactionByHash" | "eth_getTransactionReceipt" => TransactionTracingIdentifiers::from_transaction_query(params_clone.clone()).ok(), + let tracing_identifiers = match method.as_str() { + "eth_call" | "eth_estimateGas" => TransactionTracingIdentifiers::from_call(request.params()).ok(), + "eth_getTransactionByHash" | "eth_getTransactionReceipt" => TransactionTracingIdentifiers::from_transaction_query(request.params()).ok(), _ => None, }; - } + Span::with(|s| { + if let Some(ref tx) = tracing_identifiers { + tx.record_span(s); + } + }); + // make span available to rpc-server + drop(middleware_enter); + request.extensions_mut().insert(span); + let future: BoxFuture<'a, MethodResponse> = Box::pin(self.service.call(request)); + (future, tracing_identifiers) + }; - let is_admin = request.extensions.is_admin(); + let tx_ref = tracing_identifiers.as_ref(); + let multicall_ref = tx_ref.and_then(|tx| tx.multicall.as_ref()); - let client = if let Some(tx_client) = tx.as_ref().and_then(|tx| tx.client.as_ref()) { - request.extensions_mut().insert(tx_client.clone()); - tx_client - } else { - request.extensions.rpc_client() - } - .to_owned(); + // track metrics + #[cfg(feature = "metrics")] + { + // started requests + metrics::inc_rpc_requests_started(&client, &method, tx_ref.map(|tx| tx.contract), tx_ref.map(|tx| tx.function), request_type); - // trace event - Span::with(|s| { - s.rec_str("rpc_id", &request.id); - s.rec_str("rpc_client", &client); - s.rec_str("rpc_method", &method); - if let Some(ref tx) = tx { - tx.record_span(s); + if let Some(tx) = tx_ref + && let Some(multicall) = tx.multicall.as_ref() + { + multicall.record_rpc_requests_started(&client, &method, request_type); } - }); - - let tx_ref = tx.as_ref(); - let multicall_ref = tx_ref.and_then(|tx| tx.multicall.as_ref()); + } tracing::info!( rpc_client = %client, - rpc_id = %request.id, + rpc_id = %request_id, rpc_method = %method, - rpc_params = %to_json_string(&request.params), + rpc_params = %request_params_str, rpc_tx_hash = %tx_ref.and_then(|tx| tx.hash).or_empty(), rpc_tx_contract = %tx_ref.map(|tx| tx.contract).or_empty(), rpc_tx_function = %tx_ref.map(|tx| tx.function).or_empty(), @@ -255,37 +257,14 @@ impl RpcServiceT for RpcMiddleware { "rpc request" ); - // track metrics - #[cfg(feature = "metrics")] - { - // started requests - metrics::inc_rpc_requests_started(&client, &method, tx_ref.map(|tx| tx.contract), tx_ref.map(|tx| tx.function), request_type); - - if let Some(tx) = tx_ref - && let Some(multicall) = tx.multicall.as_ref() - { - multicall.record_rpc_requests_started(&client, &method, request_type); - } - - // active requests - if let Some(guard) = request.extensions.get::() { - let active = guard.max_connections() - guard.available_connections(); - metrics::set_rpc_requests_active(active as u64); - } - } - - // make span available to rpc-server - drop(middleware_enter); - request.extensions_mut().insert(span); - - let id = request.id.to_string(); + let id = request_id.to_string(); - let future_response = reject_client(&client, request.id.clone()).unwrap_or(Box::pin(self.service.call(request))); + let future_response = reject_client(&client, request_id.clone()).unwrap_or(future); RpcResponse { client, id, method: method.to_string(), - tx, + tx: tracing_identifiers, start: Instant::now(), future_response, } @@ -433,8 +412,7 @@ impl Future for RpcResponse<'_> { // Helpers // ----------------------------------------------------------------------------- -struct TransactionTracingIdentifiers { - pub client: Option, +pub struct TransactionTracingIdentifiers { pub hash: Option, pub contract: ContractName, pub function: SoliditySignature, @@ -446,16 +424,15 @@ struct TransactionTracingIdentifiers { impl TransactionTracingIdentifiers { /// eth_sendRawTransaction - fn from_raw_transaction(decoded_tx: &TransactionInput) -> anyhow::Result { + pub fn from_transaction_input(input: &TransactionInput) -> anyhow::Result { Ok(Self { - client: None, - hash: Some(decoded_tx.transaction_info.hash), - contract: codegen::contract_name(&decoded_tx.execution_info.to), - function: codegen::function_sig(&decoded_tx.execution_info.input), - from: decoded_tx.execution_info.signer.address(), - to: decoded_tx.execution_info.to, - nonce: Some(decoded_tx.execution_info.nonce), - multicall: MulticallInfo::decode_opt(decoded_tx.execution_info.to, &decoded_tx.execution_info.input), + hash: Some(input.transaction_info.hash), + contract: codegen::contract_name(&input.execution_info.to), + function: codegen::function_sig(&input.execution_info.input), + from: input.execution_info.signer.address(), + to: input.execution_info.to, + nonce: Some(input.execution_info.nonce), + multicall: MulticallInfo::decode_opt(input.execution_info.to, &input.execution_info.input), }) } @@ -463,7 +440,6 @@ impl TransactionTracingIdentifiers { fn from_call(params: Params) -> anyhow::Result { let (_, call) = next_rpc_param::(params.sequence())?; Ok(Self { - client: None, hash: None, contract: codegen::contract_name(&call.to), function: codegen::function_sig(&call.data), @@ -478,7 +454,6 @@ impl TransactionTracingIdentifiers { fn from_transaction_query(params: Params) -> anyhow::Result { let (_, hash) = next_rpc_param::(params.sequence())?; Ok(Self { - client: None, hash: Some(hash), contract: metrics::LABEL_MISSING, function: metrics::LABEL_MISSING, diff --git a/src/eth/rpc/mod.rs b/src/eth/rpc/mod.rs index 407924e89..ffb3a674e 100644 --- a/src/eth/rpc/mod.rs +++ b/src/eth/rpc/mod.rs @@ -16,7 +16,6 @@ pub use middleware::RpcHttpMiddleware; pub use middleware::RpcMiddleware; use parser::next_rpc_param; use parser::next_rpc_param_or_default; -use parser::parse_rpc_rlp; pub use server::Server; pub use subscriptions::RpcSubscriptions; pub use types::BlockFilter; diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index 6518a43ac..25b65fcb4 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -11,11 +11,15 @@ use alloy_primitives::U256; use alloy_rpc_types_trace::geth::GethDebugTracingOptions; use alloy_rpc_types_trace::geth::GethTrace; use anyhow::Result; +use futures::FutureExt; +use futures::future::BoxFuture; use futures::join; use http::Method; use itertools::Itertools; use jsonrpsee::Extensions; +use jsonrpsee::IntoResponse; use jsonrpsee::IntoSubscriptionCloseResponse; +use jsonrpsee::MethodResponse; use jsonrpsee::PendingSubscriptionSink; use jsonrpsee::server::BatchRequestConfig; use jsonrpsee::server::RandomStringIdProvider; @@ -24,7 +28,9 @@ use jsonrpsee::server::Server as RpcServer; use jsonrpsee::server::ServerConfig; use jsonrpsee::server::ServerHandle; use jsonrpsee::server::middleware::http::ProxyGetRequestLayer; +use jsonrpsee::types::Id; use jsonrpsee::types::Params; +use jsonrpsee::types::Request; use jsonrpsee::ws_client::RpcServiceBuilder; use parking_lot::RwLock; use serde_json::json; @@ -66,10 +72,12 @@ use crate::eth::rpc::RpcHttpMiddleware; use crate::eth::rpc::RpcMiddleware; use crate::eth::rpc::RpcServerConfig; use crate::eth::rpc::RpcSubscriptions; +use crate::eth::rpc::middleware::TransactionTracingIdentifiers; use crate::eth::rpc::middleware::decode_input_arguments; use crate::eth::rpc::next_rpc_param; use crate::eth::rpc::next_rpc_param_or_default; use crate::eth::rpc::parser::RpcExtensionsExt; +use crate::eth::rpc::parser::parse_rpc_rlp; use crate::eth::rpc::subscriptions::RpcSubscriptionsHandles; use crate::eth::storage::ExecutionKind; use crate::eth::storage::StorageError; @@ -180,19 +188,19 @@ impl Server { ); // configure context - let ctx = RpcContext { + let ctx = Arc::new(RpcContext { server: Arc::new(this.clone()), client_version: "stratus", subs: Arc::clone(&subs.connected), - }; + }); // configure module - let mut module = RpcModule::::new(ctx); + let mut module = RpcModule::::from_arc(Arc::clone(&ctx)); module = register_methods(module)?; // configure middleware let cors = CorsLayer::new().allow_methods([Method::POST]).allow_origin(Any).allow_headers(Any); - let rpc_middleware = RpcServiceBuilder::new().layer_fn(RpcMiddleware::new); + let rpc_middleware = RpcServiceBuilder::new().layer_fn(move |service| RpcMiddleware::new(service, Arc::clone(&ctx))); let http_middleware = tower::ServiceBuilder::new().layer(cors).layer_fn(RpcHttpMiddleware::new).layer( ProxyGetRequestLayer::new([ ("/health", "stratus_health"), @@ -347,7 +355,6 @@ fn register_methods(mut module: RpcModule) -> anyhow::Result, ctx: Arc, ext: Extensions .inspect_err(|e| tracing::warn!(reason = ?e, "failed to execute stratus_accessList")) } -fn eth_send_raw_transaction(_: Params<'_>, ctx: Arc, ext: Extensions) -> Result { +pub fn eth_send_raw_transaction<'a>( + mut request: Request<'a>, + ctx: Arc, + span: Span, +) -> Result<(BoxFuture<'a, MethodResponse>, Option)> { + let enter = span.enter(); + let params = request.params(); + let id = request.id().into_owned(); + let (params, data) = next_rpc_param::(params.sequence())?; + let (_, access_list) = next_rpc_param::>(params)?; + let input = parse_rpc_rlp::(&data)?; + let tracing_identifiers = TransactionTracingIdentifiers::from_transaction_input(&input).ok(); + + Span::with(|s| { + if let Some(ref tx) = tracing_identifiers { + tx.record_span(s); + } + }); + drop(enter); + + request.extensions_mut().insert(span); + + let ext = request.extensions; + let ext_clone = ext.clone(); + + let future = tokio::task::spawn_blocking(move || { + let rp = _eth_send_raw_transaction_impl(input, data, access_list, ctx, ext).into_response(); + MethodResponse::response(id, rp, usize::MAX) + }) + .map(|result| match result { + Ok(r) => r, + Err(err) => { + tracing::error!("Join error for blocking RPC method: {:?}", err); + MethodResponse::error( + Id::Null, + StratusError::Unexpected(crate::eth::types::UnexpectedError::Unexpected(anyhow::anyhow!(err))), + ) + .with_extensions(ext_clone) + } + }) + .boxed(); + Ok((future, tracing_identifiers)) +} + +fn _eth_send_raw_transaction_impl( + tx: TransactionInput, + data: Bytes, + access_list: Option, + ctx: Arc, + ext: Extensions, +) -> Result { // enter span let _middleware_enter = ext.enter_middleware_span(); let _method_enter = info_span!( @@ -1327,17 +1384,6 @@ fn eth_send_raw_transaction(_: Params<'_>, ctx: Arc, ext: Extensions ) .entered(); - // get the pre-decoded transaction from extensions - let (tx, tx_data, access_list) = match (ext.get::(), ext.get::(), ext.get::>()) { - (Some(tx), Some(data), access_list) => (tx.clone(), data.clone(), access_list.cloned().flatten()), - _ => { - tracing::error!("failed to execute eth_sendRawTransaction because transaction input is not available"); - return Err(RpcError::TransactionInvalid { - decode_error: "transaction input is not available".to_string(), - } - .into()); - } - }; let tx_hash = tx.transaction_info.hash; // track @@ -1371,7 +1417,7 @@ fn eth_send_raw_transaction(_: Params<'_>, ctx: Arc, ext: Extensions } }, NodeMode::Follower => match &ctx.server.read_importer() { - Some(importer) => match Handle::current().block_on(importer.forward_to_leader(tx, tx_hash, tx_data)) { + Some(importer) => match Handle::current().block_on(importer.forward_to_leader(tx, tx_hash, data)) { Ok(hash) => Ok(hex_data(hash)), Err(e) => Err(e), }, From 867bf07aeb5023bd0a64f30a75ec422db9d45e10 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Wed, 2 Sep 2026 18:30:35 -0300 Subject: [PATCH 24/31] resolve error to future --- src/eth/rpc/middleware/rpc_middleware.rs | 9 ++++++++- src/eth/rpc/server.rs | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/eth/rpc/middleware/rpc_middleware.rs b/src/eth/rpc/middleware/rpc_middleware.rs index 993739a9a..bea8be6c0 100644 --- a/src/eth/rpc/middleware/rpc_middleware.rs +++ b/src/eth/rpc/middleware/rpc_middleware.rs @@ -205,7 +205,14 @@ impl RpcServiceT for RpcMiddleware { let (future, tracing_identifiers) = if method == "eth_sendRawTransaction" { drop(middleware_enter); - eth_send_raw_transaction(request, Arc::clone(&self.ctx), span).unwrap() + match eth_send_raw_transaction(request, Arc::clone(&self.ctx), span) { + Ok(result) => result, + Err(err) => { + tracing::warn!(?err, "failed to parse eth_sendRawTransaction request"); + let future: BoxFuture<'a, MethodResponse> = Box::pin(err.to_response_future(request_id.clone())); + (future, None) + } + } } else { let tracing_identifiers = match method.as_str() { "eth_call" | "eth_estimateGas" => TransactionTracingIdentifiers::from_call(request.params()).ok(), diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index 9c66828a7..6411d8cf8 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -1326,12 +1326,12 @@ pub fn eth_send_raw_transaction<'a>( mut request: Request<'a>, ctx: Arc, span: Span, -) -> Result<(BoxFuture<'a, MethodResponse>, Option)> { +) -> Result<(BoxFuture<'a, MethodResponse>, Option), StratusError> { let enter = span.enter(); let params = request.params(); let id = request.id().into_owned(); let (params, data) = next_rpc_param::(params.sequence())?; - let (_, access_list) = next_rpc_param::>(params)?; + let (_, access_list) = next_rpc_param_or_default::>(params)?; let input = parse_rpc_rlp::(&data)?; let tracing_identifiers = TransactionTracingIdentifiers::from_transaction_input(&input).ok(); From 0abb490ee6c19d37afd58bc698aa92ac9f826316 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Thu, 3 Sep 2026 12:57:05 -0300 Subject: [PATCH 25/31] rework call execution input creation --- .../evm/types/input/call_execution.rs | 23 +---- .../evm/types/input/transaction_execution.rs | 12 +-- src/eth/executor/mod.rs | 42 +++----- src/eth/follower/importer/mod.rs | 2 +- .../permanent/rocks/rocks_permanent.rs | 4 +- .../rocks/types/transaction_mined.rs | 9 +- src/eth/storage/resolve_pending.rs | 40 +++----- src/eth/storage/stratus_storage.rs | 95 ++++++++++++++----- src/eth/storage/temporary/inmemory/mod.rs | 4 +- .../storage/temporary/inmemory/transaction.rs | 9 +- src/eth/types/block/block_info.rs | 34 +++++++ src/eth/types/block/mod.rs | 4 +- src/eth/types/block/pending_block.rs | 6 +- src/eth/types/block/pending_block_header.rs | 18 ---- src/eth/types/execution_kind.rs | 15 ++- src/eth/types/mod.rs | 2 +- src/eth/types/primitives/unix_time_now.rs | 7 ++ src/infra/metrics/metrics_definitions.rs | 6 -- 18 files changed, 185 insertions(+), 147 deletions(-) create mode 100644 src/eth/types/block/block_info.rs delete mode 100644 src/eth/types/block/pending_block_header.rs diff --git a/src/eth/executor/evm/types/input/call_execution.rs b/src/eth/executor/evm/types/input/call_execution.rs index 3bedc29bc..5b751fef3 100644 --- a/src/eth/executor/evm/types/input/call_execution.rs +++ b/src/eth/executor/evm/types/input/call_execution.rs @@ -8,11 +8,10 @@ use crate::eth::executor::evm::types::GAS_MAX_LIMIT; use crate::eth::executor::evm::types::GeneralRevm; use crate::eth::storage::ExecutionKind; use crate::eth::types::Address; -use crate::eth::types::BlockHeader; +use crate::eth::types::BlockInfo; use crate::eth::types::BlockNumber; use crate::eth::types::Bytes; use crate::eth::types::CallInput; -use crate::eth::types::PendingBlockHeader; use crate::eth::types::UnixTime; use crate::eth::types::Wei; use crate::ext::OptionExt; @@ -60,28 +59,14 @@ pub struct CallExecutionInput { } impl CallExecutionInput { - /// Creates from a call that was sent directly to Stratus with `eth_call` or `eth_estimateGas` for a pending block. - pub fn from_pending_block(input: CallInput, block: PendingBlockHeader, kind: ExecutionKind) -> Self { + pub fn create(input: CallInput, block_info: BlockInfo, kind: ExecutionKind) -> Self { Self { from: input.from.unwrap_or(Address::ZERO), to: input.to.map_into(), value: input.value, data: input.data, - block_number: block.number, - block_timestamp: *block.timestamp, - kind, - } - } - - /// Creates from a call that was sent directly to Stratus with `eth_call` or `eth_estimateGas` for a mined block. - pub fn from_mined_block(input: CallInput, block: BlockHeader, kind: ExecutionKind) -> Self { - Self { - from: input.from.unwrap_or(Address::ZERO), - to: input.to.map_into(), - value: input.value, - data: input.data, - block_number: block.number, - block_timestamp: block.timestamp, + block_number: block_info.number, + block_timestamp: *block_info.timestamp, kind, } } diff --git a/src/eth/executor/evm/types/input/transaction_execution.rs b/src/eth/executor/evm/types/input/transaction_execution.rs index 11e233f77..0b70fd35c 100644 --- a/src/eth/executor/evm/types/input/transaction_execution.rs +++ b/src/eth/executor/evm/types/input/transaction_execution.rs @@ -8,12 +8,12 @@ use crate::eth::executor::evm::types::GAS_MAX_LIMIT; use crate::eth::executor::evm::types::GeneralRevm; use crate::eth::storage::ExecutionKind; use crate::eth::types::Address; +use crate::eth::types::BlockInfo; use crate::eth::types::BlockNumber; use crate::eth::types::Bytes; use crate::eth::types::ChainId; use crate::eth::types::Gas; use crate::eth::types::Nonce; -use crate::eth::types::PendingBlockHeader; use crate::eth::types::TransactionInput; use crate::eth::types::UnixTime; use crate::eth::types::Wei; @@ -81,7 +81,7 @@ pub struct TransactionExecutionInput { impl TransactionExecutionInput { /// Creates from a transaction that was sent to Stratus with `eth_sendRawTransaction` or during Importing. - pub fn from_eth_transaction(input: &TransactionInput, block_number: BlockNumber, block_timestamp: UnixTime) -> Self { + pub fn create(input: &TransactionInput, block_info: BlockInfo) -> Self { Self { from: input.signer(), to: input.execution_info.to, @@ -90,16 +90,16 @@ impl TransactionExecutionInput { gas_limit: input.execution_info.gas_limit, gas_price: input.execution_info.gas_price, nonce: input.execution_info.nonce, - block_number, - block_timestamp, + block_number: block_info.number, + block_timestamp: *block_info.timestamp, chain_id: input.execution_info.chain_id, kind: ExecutionKind::Transaction, } } } -impl PartialEq<&PendingBlockHeader> for TransactionExecutionInput { - fn eq(&self, other: &&PendingBlockHeader) -> bool { +impl PartialEq for TransactionExecutionInput { + fn eq(&self, other: &BlockInfo) -> bool { self.block_number == other.number && self.block_timestamp == *other.timestamp } } diff --git a/src/eth/executor/mod.rs b/src/eth/executor/mod.rs index 27ce78ba2..07bf219f0 100644 --- a/src/eth/executor/mod.rs +++ b/src/eth/executor/mod.rs @@ -40,7 +40,6 @@ use crate::eth::executor::evm::types::SlotAccessMetrics; use crate::eth::executor::evm_worker_pool::EvmWorkerPool; use crate::eth::executor::types::EvmRoute; use crate::eth::miner::Miner; -use crate::eth::rpc::RpcError; use crate::eth::storage::ExecutionKind; use crate::eth::storage::StorageError; use crate::eth::storage::StratusStorage; @@ -177,8 +176,8 @@ impl Executor { tracing::info!(%block_number, tx_hash = %tx.hash(), "reexecuting external transaction"); let tx_input: TransactionInput = tx.try_into()?; - let pending_block = self.storage.read_pending_block_header(); - let mut evm_input = TransactionExecutionInput::from_eth_transaction(&tx_input, pending_block.number, *pending_block.timestamp); + let pending_header = self.storage.read_pending_block_header(); + let mut evm_input = TransactionExecutionInput::create(&tx_input, pending_header); // when transaction externally failed, create fake transaction instead of reexecuting let (tx_execution, state) = match receipt.is_success() { @@ -357,7 +356,7 @@ impl Executor { // prepare evm input let pending_header = self.storage.read_pending_block_header(); - let evm_input = TransactionExecutionInput::from_eth_transaction(&tx_input, pending_header.number, *pending_header.timestamp); + let evm_input = TransactionExecutionInput::create(&tx_input, pending_header); // execute transaction in evm (retry only in case of conflict, but do not retry on other failures) tracing::debug!( @@ -424,8 +423,6 @@ impl Executor { { #[cfg(feature = "metrics")] let start = metrics::now(); - let point_in_time = kind.point_in_time(); - Span::with(|s| { s.rec_opt("from", &call_input.from); s.rec_opt("to", &call_input.to); @@ -435,40 +432,25 @@ impl Executor { to = ?call_input.to, data_len = call_input.data.len(), data = %call_input.data, - %point_in_time, + ?kind, "executing read-only local transaction" ); #[cfg(feature = "metrics")] let (function, contract) = { (codegen::function_sig(&call_input.data), codegen::contract_name(&call_input.to)) }; - // execute - let evm_input = match kind { - ExecutionKind::Transaction | ExecutionKind::RPC(PointInTime::Pending) | ExecutionKind::AccessList => { - let pending_header = self.storage.read_pending_block_header(); - CallExecutionInput::from_pending_block(call_input, pending_header, kind) - } - ExecutionKind::CallLatest(block_number) | ExecutionKind::CallPast(block_number) => { - let Some(block) = self.storage.read_block(crate::eth::rpc::BlockFilter::Number(block_number))? else { - return Err(RpcError::BlockFilterInvalid { - filter: crate::eth::rpc::BlockFilter::Number(block_number), - } - .into()); - }; - CallExecutionInput::from_mined_block(call_input, block.header, kind) - } - ExecutionKind::RPC(pit) => { - let Some(block) = self.storage.read_block(pit.into())? else { - return Err(RpcError::BlockFilterInvalid { filter: pit.into() }.into()); - }; - CallExecutionInput::from_mined_block(call_input, block.header, kind) - } + let filter = kind.into(); + let Some(block_info) = self.storage.read_block_info(filter)? else { + return Err(StorageError::BlockNotFound { filter }.into()); }; - let evm_route = match point_in_time { - PointInTime::Pending | PointInTime::Latest => EvmRoute::CallPresent(evm_input), // // route using execution kind rather than pit + let evm_input = CallExecutionInput::create(call_input, block_info, kind); + + let evm_route = match kind.point_in_time() { + PointInTime::Pending | PointInTime::Latest => EvmRoute::CallPresent(evm_input), PointInTime::Past(_) => EvmRoute::CallPast(evm_input), }; + let evm_result = self.evms.execute::(evm_route); // track metrics diff --git a/src/eth/follower/importer/mod.rs b/src/eth/follower/importer/mod.rs index e4b1d7a04..598c23aa6 100644 --- a/src/eth/follower/importer/mod.rs +++ b/src/eth/follower/importer/mod.rs @@ -315,7 +315,7 @@ mod tests { /// Mines a block applying `changes` (mirrors the helper in `stratus_storage` tests). fn mine_block(storage: &StratusStorage, state: State) { let header = storage.read_pending_block_header(); - let evm_input = TransactionExecutionInput::from_eth_transaction(&TransactionInput::default(), header.number, *header.timestamp); + let evm_input = TransactionExecutionInput::create(&TransactionInput::default(), header); let result = TransactionExecutionResult { result: ExecutionResult::Success, diff --git a/src/eth/storage/permanent/rocks/rocks_permanent.rs b/src/eth/storage/permanent/rocks/rocks_permanent.rs index 133158478..76734e227 100644 --- a/src/eth/storage/permanent/rocks/rocks_permanent.rs +++ b/src/eth/storage/permanent/rocks/rocks_permanent.rs @@ -140,11 +140,11 @@ impl RocksPermanentStorage { // ------------------------------------------------------------------------- pub fn read_mined_block_number(&self) -> BlockNumber { - self.block_number.load(Ordering::SeqCst).into() + self.block_number.load(Ordering::Acquire).into() } pub fn set_mined_block_number(&self, number: BlockNumber) { - self.block_number.store(number.as_u32(), Ordering::SeqCst); + self.block_number.store(number.as_u32(), Ordering::Release); } pub fn has_genesis(&self) -> Result { diff --git a/src/eth/storage/permanent/rocks/types/transaction_mined.rs b/src/eth/storage/permanent/rocks/types/transaction_mined.rs index 1372584b9..68b62c4d0 100644 --- a/src/eth/storage/permanent/rocks/types/transaction_mined.rs +++ b/src/eth/storage/permanent/rocks/types/transaction_mined.rs @@ -11,6 +11,7 @@ use crate::eth::executor::TransactionExecutionInput; use crate::eth::executor::TransactionExecutionResult; use crate::eth::storage::permanent::rocks::SerializeDeserializeWithContext; use crate::eth::storage::permanent::rocks::types::execution_result::ExecutionResultBuilder; +use crate::eth::types::BlockInfo; use crate::eth::types::Index; use crate::eth::types::MinedData; use crate::eth::types::TransactionInput; @@ -87,7 +88,13 @@ impl TransactionMined { deployed_contract_address: other.execution.deployed_contract_address.map_into(), }; - let evm_input = TransactionExecutionInput::from_eth_transaction(&input, block_number.into(), other.execution.block_timestamp.into()); + let evm_input = TransactionExecutionInput::create( + &input, + BlockInfo { + number: block_number.into(), + timestamp: other.execution.block_timestamp.into(), + }, + ); let execution = TransactionExecution { info: input.transaction_info, signature: input.signature, diff --git a/src/eth/storage/resolve_pending.rs b/src/eth/storage/resolve_pending.rs index bb124dd21..cc9d7c85b 100644 --- a/src/eth/storage/resolve_pending.rs +++ b/src/eth/storage/resolve_pending.rs @@ -1,8 +1,7 @@ -use parking_lot::RwLockReadGuard; - use crate::eth::storage::ExecutionKind; use crate::eth::storage::StratusStorage; use crate::eth::storage::stratus_storage::EntityRead; +use crate::eth::storage::stratus_storage::LatestStateReadGuard; use crate::eth::types::Account; use crate::eth::types::BlockNumber; use crate::eth::types::PointInTime; @@ -27,27 +26,19 @@ struct SealPrivate; #[derive(Debug, strum::Display)] pub enum MinedPointInTime<'a> { #[strum(to_string = "latest")] - Latest(Seal, Option>), + Latest(Seal, Option>), #[strum(to_string = "past")] Past(Seal, BlockNumber), } impl<'a> MinedPointInTime<'a> { - fn latest(guard: Option>) -> Self { + fn latest(guard: Option>) -> Self { Self::Latest(Seal(SealPrivate), guard) } fn past(number: BlockNumber) -> Self { Self::Past(Seal(SealPrivate), number) } - - /// Extracts the read guard if present, leaving `Mined(None)` in its place. - fn take_guard(&mut self) -> Option> { - match self { - Self::Latest(_, guard) => guard.take(), - Self::Past(_, _) => None, - } - } } impl From> for MetricLabelValue { @@ -56,15 +47,6 @@ impl From> for MetricLabelValue { } } -/// Unlocks the guard fairly when dropped. -impl<'a> Drop for MinedPointInTime<'a> { - fn drop(&mut self) { - if let Some(guard) = self.take_guard() { - RwLockReadGuard::unlock_fair(guard); - } - } -} - /// Outcome of resolving pending state for a read. #[derive(Debug)] pub(super) enum Resolved<'a, T> { @@ -92,9 +74,8 @@ impl Resolve for Slot {} impl StratusStorage { fn resolve_call_point(&self, block_number: BlockNumber) -> MinedPointInTime<'_> { - let guard = self.transient_state_lock.read(); - let mined = self.read_mined_block_number(); - if block_number >= mined { + let guard = self.latest_state_lock.read(); + if block_number >= guard.number { MinedPointInTime::latest(Some(guard)) } else { MinedPointInTime::past(block_number) @@ -136,12 +117,15 @@ mod tests { let resolved = Slot::resolve(&storage, (address, index), kind); match resolved { - super::Resolved::Miss(mut point) => { + super::Resolved::Miss(point) => { assert!( matches!(point, super::MinedPointInTime::Latest(_, _)), "Full call should read latest while block is the mined tip" ); - assert!(point.take_guard().is_some(), "guard should be held for valid latest read"); + assert!( + matches!(point, super::MinedPointInTime::Latest(_, Some(_))), + "guard should be held for valid latest read" + ); } other => panic!("expected Miss, got {other:?}"), } @@ -152,7 +136,7 @@ mod tests { // Stale: b=5 < mined=6. Full → MinedPast(5), NOT MinedPast(4). let resolved = Slot::resolve(&storage, (address, index), kind); match resolved { - super::Resolved::Miss(mut point) => { + super::Resolved::Miss(point) => { assert!(!matches!(point, super::MinedPointInTime::Latest(_, _)), "stale call should not read latest"); match &point { super::MinedPointInTime::Past(_, number) => { @@ -160,7 +144,7 @@ mod tests { } other => panic!("expected Past, got {other:?}"), } - assert!(point.take_guard().is_none(), "no guard for historical read"); + assert!(matches!(point, super::MinedPointInTime::Latest(_, None)), "no guard for historical read"); } other => panic!("expected Miss, got {other:?}"), } diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index 3ebedc743..aa76d00a0 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -23,6 +23,7 @@ use crate::eth::storage::resolve_pending; use crate::eth::types::Account; use crate::eth::types::Address; use crate::eth::types::Block; +use crate::eth::types::BlockInfo; use crate::eth::types::BlockNumber; #[cfg(feature = "dev")] use crate::eth::types::Bytes; @@ -32,7 +33,6 @@ use crate::eth::types::LogMessage; #[cfg(feature = "dev")] use crate::eth::types::Nonce; use crate::eth::types::PendingBlock; -use crate::eth::types::PendingBlockHeader; use crate::eth::types::PointInTime; use crate::eth::types::Slot; use crate::eth::types::SlotIndex; @@ -54,6 +54,48 @@ mod label { pub(super) const CACHE: &str = "cache"; } +pub struct LatestStateLock(parking_lot::RwLock); +// could use ManuallyDrop instead +#[derive(Debug)] +pub struct LatestStateReadGuard<'a>(Option>); +pub struct LatestStateWriteGuard<'a>(parking_lot::RwLockWriteGuard<'a, BlockInfo>); + +impl<'a> LatestStateWriteGuard<'a> { + fn set_latest_block_info(&mut self, block_info: BlockInfo) { + (*self.0) = block_info; + } +} + +impl LatestStateLock { + fn new(block_info: BlockInfo) -> Self { + Self(parking_lot::RwLock::new(block_info)) + } + + pub fn read<'a>(&'a self) -> LatestStateReadGuard<'a> { + LatestStateReadGuard(Some(self.0.read())) + } + + pub fn write<'a>(&'a self) -> LatestStateWriteGuard<'a> { + LatestStateWriteGuard(self.0.write()) + } +} + +impl std::ops::Deref for LatestStateReadGuard<'_> { + type Target = BlockInfo; + fn deref(&self) -> &BlockInfo { + #[allow(clippy::expect_used)] + self.0.as_ref().expect("guard present until dropped") + } +} + +impl Drop for LatestStateReadGuard<'_> { + fn drop(&mut self) { + if let Some(guard) = self.0.take() { + parking_lot::RwLockReadGuard::unlock_fair(guard); + } + } +} + /// Proxy that simplifies interaction with permanent and temporary storages. /// /// Additionaly it tracks metrics that are independent of the storage implementation. @@ -61,9 +103,9 @@ pub struct StratusStorage { temp: InMemoryTemporaryStorage, cache: StorageCache, pub perm: RocksPermanentStorage, - // CONTRACT: Always acquire a lock when reading slots or accounts from latest (cache OR perm) and when saving a block - // TODO: store latest mined block header in this lock - pub(super) transient_state_lock: parking_lot::RwLock<()>, + // CONTRACT: Always acquire a lock when reading slots or accounts from latest (cache OR perm) and when saving a block. + // The value in the lock is the latest block execution information. + pub(super) latest_state_lock: LatestStateLock, #[cfg(feature = "dev")] perm_config: crate::eth::storage::permanent::PermanentStorageConfig, } @@ -235,11 +277,15 @@ impl StratusStorage { cache: StorageCache, #[cfg(feature = "dev")] perm_config: crate::eth::storage::permanent::PermanentStorageConfig, ) -> Result { + let latest_block = perm + .read_block(BlockFilter::Latest)? + .ok_or(StorageError::BlockNotFound { filter: BlockFilter::Latest })?; + let this = Self { temp, cache, perm, - transient_state_lock: parking_lot::RwLock::new(()), + latest_state_lock: LatestStateLock::new(latest_block.header.into()), #[cfg(feature = "dev")] perm_config, }; @@ -319,24 +365,12 @@ impl StratusStorage { Ok(number) } - pub fn read_pending_block_header(&self) -> PendingBlockHeader { - #[cfg(feature = "tracing")] - let _span = tracing::info_span!("storage::read_pending_block_number").entered(); - tracing::debug!(storage = %label::TEMP, "reading pending block number"); - - timed(|| self.temp.read_pending_block_header()).with(|m| { - metrics::inc_storage_read_pending_block_number(m.elapsed, label::TEMP, true); - }) + pub fn read_pending_block_header(&self) -> BlockInfo { + self.temp.read_pending_block_header() } pub fn read_mined_block_number(&self) -> BlockNumber { - #[cfg(feature = "tracing")] - let _span = tracing::info_span!("storage::read_mined_block_number").entered(); - tracing::debug!(storage = %label::PERM, "reading mined block number"); - - timed(|| self.perm.read_mined_block_number()).with(|m| { - metrics::inc_storage_read_mined_block_number(m.elapsed, label::PERM, true); - }) + self.perm.read_mined_block_number() } pub fn set_pending_from_external(&self, block: &ExternalBlock) { @@ -534,8 +568,10 @@ impl StratusStorage { let tens_of_millions_gas_used = block.header.gas_used.as_u64() / 10_000_000; timed(|| { - let guard = self.transient_state_lock.write(); + let mut guard = self.latest_state_lock.write(); + let block_info = (&block.header).into(); self.perm.save_block(block, changes.finalize())?; + guard.set_latest_block_info(block_info); self.cache.cache_account_and_slots_latest_from_changes(changes); drop(guard); Ok(()) @@ -565,6 +601,20 @@ impl StratusStorage { }) } + pub fn read_block_info(&self, filter: BlockFilter) -> Result, StorageError> { + let latest_state = self.latest_state_lock.read(); + match filter { + BlockFilter::Pending => Ok(Some(self.read_pending_block_header())), + BlockFilter::Latest => Ok(Some(*latest_state)), + BlockFilter::Number(number) if number == self.read_mined_block_number() => self.read_block_info(BlockFilter::Latest), + BlockFilter::Number(number) if number == self.read_mined_block_number().next_block_number() => self.read_block_info(BlockFilter::Pending), + _ => { + drop(latest_state); + Ok(self.read_block(filter)?.map(|block| block.header.into())) + } + } + } + pub fn read_block_with_changes(&self, filter: BlockFilter) -> Result, StorageError> { #[cfg(feature = "tracing")] let _span = tracing::info_span!("storage::read_block_with_changes", %filter).entered(); @@ -811,6 +861,7 @@ impl StratusStorage { BlockFilter::Pending => Ok(PointInTime::Pending), BlockFilter::Latest => Ok(PointInTime::Latest), BlockFilter::Earliest => Ok(PointInTime::Past(BlockNumber::ZERO)), + // if number == latest (/pending) should we return PointInTime::Latest (/Pending) ? BlockFilter::Number(number) => Ok(PointInTime::Past(number)), BlockFilter::Hash(_) | BlockFilter::Timestamp(_) => self .read_block(block_filter)? @@ -882,7 +933,7 @@ mod tests { /// Saves an execution applying `changes` to the pending block, without finishing it. fn save_execution(storage: &StratusStorage, changes: State) { let header = storage.read_pending_block_header(); - let evm_input = TransactionExecutionInput::from_eth_transaction(&TransactionInput::default(), header.number, *header.timestamp); + let evm_input = TransactionExecutionInput::create(&TransactionInput::default(), header); let result = TransactionExecutionResult { result: ExecutionResult::Success, diff --git a/src/eth/storage/temporary/inmemory/mod.rs b/src/eth/storage/temporary/inmemory/mod.rs index d3530a04e..65d381d15 100644 --- a/src/eth/storage/temporary/inmemory/mod.rs +++ b/src/eth/storage/temporary/inmemory/mod.rs @@ -7,6 +7,7 @@ use crate::eth::storage::StorageError; use crate::eth::storage::temporary::inmemory::transaction::InmemoryTransactionTemporaryStorage; use crate::eth::types::Account; use crate::eth::types::Address; +use crate::eth::types::BlockInfo; use crate::eth::types::BlockNumber; #[cfg(feature = "dev")] use crate::eth::types::Bytes; @@ -14,7 +15,6 @@ use crate::eth::types::Hash; #[cfg(feature = "dev")] use crate::eth::types::Nonce; use crate::eth::types::PendingBlock; -use crate::eth::types::PendingBlockHeader; use crate::eth::types::Slot; use crate::eth::types::SlotIndex; use crate::eth::types::UnixTime; @@ -35,7 +35,7 @@ impl InMemoryTemporaryStorage { } } - pub fn read_pending_block_header(&self) -> PendingBlockHeader { + pub fn read_pending_block_header(&self) -> BlockInfo { self.transaction_storage.read_pending_block_header() } diff --git a/src/eth/storage/temporary/inmemory/transaction.rs b/src/eth/storage/temporary/inmemory/transaction.rs index 4269bc96b..5f120aeb6 100644 --- a/src/eth/storage/temporary/inmemory/transaction.rs +++ b/src/eth/storage/temporary/inmemory/transaction.rs @@ -15,6 +15,7 @@ use crate::eth::storage::StorageError; use crate::eth::storage::temporary::inmemory::InMemoryTemporaryStorageState; use crate::eth::types::Account; use crate::eth::types::Address; +use crate::eth::types::BlockInfo; use crate::eth::types::BlockNumber; #[cfg(feature = "dev")] use crate::eth::types::Bytes; @@ -22,7 +23,6 @@ use crate::eth::types::Hash; #[cfg(feature = "dev")] use crate::eth::types::Nonce; use crate::eth::types::PendingBlock; -use crate::eth::types::PendingBlockHeader; use crate::eth::types::Slot; use crate::eth::types::SlotIndex; use crate::eth::types::TransactionInput; @@ -59,7 +59,7 @@ impl InmemoryTransactionTemporaryStorage { // Block number // ------------------------------------------------------------------------- - pub fn read_pending_block_header(&self) -> PendingBlockHeader { + pub fn read_pending_block_header(&self) -> BlockInfo { let pending_block = self.pending_block.read(); pending_block.block.header } @@ -77,11 +77,10 @@ impl InmemoryTransactionTemporaryStorage { pub fn save_pending_execution(&self, tx: TransactionExecution, state: State) -> Result<(), StorageError> { // check conflicts let pending_block = self.pending_block.upgradable_read(); - if tx.input != &pending_block.block.header { + if tx.input != pending_block.block.header { let actual_input = tx.input.clone(); let tx_input: TransactionInput = tx.into(); - let expected_input = - TransactionExecutionInput::from_eth_transaction(&tx_input, pending_block.block.header.number, *pending_block.block.header.timestamp); + let expected_input = TransactionExecutionInput::create(&tx_input, pending_block.block.header); return Err(StorageError::EvmInputMismatch { expected: Box::new(expected_input), actual: Box::new(actual_input), diff --git a/src/eth/types/block/block_info.rs b/src/eth/types/block/block_info.rs new file mode 100644 index 000000000..5217b888e --- /dev/null +++ b/src/eth/types/block/block_info.rs @@ -0,0 +1,34 @@ +use display_json::DebugAsJson; + +use crate::eth::types::BlockHeader; +use crate::eth::types::BlockNumber; +use crate::eth::types::UnixTimeNow; + +/// Block information used on evm executions +#[derive(DebugAsJson, Clone, Copy, Default, serde::Serialize)] +pub struct BlockInfo { + pub number: BlockNumber, + pub timestamp: UnixTimeNow, +} + +impl BlockInfo { + /// Creates a new [`BlockInfo`] with the specified number and the current timestamp. + pub fn new_at_now(number: BlockNumber) -> Self { + Self { number, ..Self::default() } + } +} + +impl From for BlockInfo { + fn from(value: BlockHeader) -> Self { + (&value).into() + } +} + +impl From<&BlockHeader> for BlockInfo { + fn from(value: &BlockHeader) -> Self { + Self { + number: value.number, + timestamp: value.timestamp.into(), + } + } +} diff --git a/src/eth/types/block/mod.rs b/src/eth/types/block/mod.rs index 319b9036b..f7a425412 100644 --- a/src/eth/types/block/mod.rs +++ b/src/eth/types/block/mod.rs @@ -1,10 +1,10 @@ #[allow(clippy::module_inception)] mod block; mod block_header; +mod block_info; mod pending_block; -mod pending_block_header; pub use block::Block; pub use block_header::BlockHeader; +pub use block_info::BlockInfo; pub use pending_block::PendingBlock; -pub use pending_block_header::PendingBlockHeader; diff --git a/src/eth/types/block/pending_block.rs b/src/eth/types/block/pending_block.rs index b67726ebc..5a371a5e8 100644 --- a/src/eth/types/block/pending_block.rs +++ b/src/eth/types/block/pending_block.rs @@ -2,14 +2,14 @@ use display_json::DebugAsJson; use indexmap::IndexMap; use crate::eth::executor::TransactionExecution; +use crate::eth::types::BlockInfo; use crate::eth::types::BlockNumber; use crate::eth::types::Hash; -use crate::eth::types::PendingBlockHeader; /// Block that is being mined and receiving updates. #[derive(DebugAsJson, Clone, Default, serde::Serialize)] pub struct PendingBlock { - pub header: PendingBlockHeader, + pub header: BlockInfo, // TODO: review why we use an indexmap here but not everywhere else pub transactions: IndexMap, } @@ -18,7 +18,7 @@ impl PendingBlock { /// Creates a new [`PendingBlock`] with the specified number. pub fn new_at_now(number: BlockNumber) -> Self { Self { - header: PendingBlockHeader::new_at_now(number), + header: BlockInfo::new_at_now(number), transactions: IndexMap::new(), } } diff --git a/src/eth/types/block/pending_block_header.rs b/src/eth/types/block/pending_block_header.rs deleted file mode 100644 index 94bcd03b9..000000000 --- a/src/eth/types/block/pending_block_header.rs +++ /dev/null @@ -1,18 +0,0 @@ -use display_json::DebugAsJson; - -use crate::eth::types::BlockNumber; -use crate::eth::types::UnixTimeNow; - -/// Header of the pending block being mined. -#[derive(DebugAsJson, Clone, Copy, Default, serde::Serialize)] -pub struct PendingBlockHeader { - pub number: BlockNumber, - pub timestamp: UnixTimeNow, -} - -impl PendingBlockHeader { - /// Creates a new [`PendingBlockHeader`] with the specified number and the current timestamp. - pub fn new_at_now(number: BlockNumber) -> Self { - Self { number, ..Self::default() } - } -} diff --git a/src/eth/types/execution_kind.rs b/src/eth/types/execution_kind.rs index 61d417cd3..aee841fa6 100644 --- a/src/eth/types/execution_kind.rs +++ b/src/eth/types/execution_kind.rs @@ -1,7 +1,10 @@ +use derive_more::Debug; + +use crate::eth::rpc::BlockFilter; use crate::eth::types::BlockNumber; use crate::eth::types::PointInTime; -#[derive(Clone, Copy, serde::Serialize, PartialEq, Default, Eq)] +#[derive(Clone, Copy, serde::Serialize, PartialEq, Default, Eq, Debug)] #[cfg_attr(test, derive(fake::Dummy))] pub enum ExecutionKind { CallLatest(BlockNumber), @@ -35,3 +38,13 @@ impl From<&ExecutionKind> for PointInTime { } } } + +impl From for BlockFilter { + fn from(value: ExecutionKind) -> Self { + match value { + ExecutionKind::Transaction | ExecutionKind::RPC(PointInTime::Pending) | ExecutionKind::AccessList => crate::eth::rpc::BlockFilter::Pending, + ExecutionKind::CallLatest(block_number) | ExecutionKind::CallPast(block_number) => crate::eth::rpc::BlockFilter::Number(block_number), + ExecutionKind::RPC(pit) => pit.into(), + } + } +} diff --git a/src/eth/types/mod.rs b/src/eth/types/mod.rs index aec40093c..170618781 100644 --- a/src/eth/types/mod.rs +++ b/src/eth/types/mod.rs @@ -6,8 +6,8 @@ pub mod primitives; pub mod transaction; pub use block::Block; pub use block::BlockHeader; +pub use block::BlockInfo; pub use block::PendingBlock; -pub use block::PendingBlockHeader; pub use error::DecodeInputError; pub use error::ErrorCode; pub use error::StateError; diff --git a/src/eth/types/primitives/unix_time_now.rs b/src/eth/types/primitives/unix_time_now.rs index 68919223a..e76fa6e24 100644 --- a/src/eth/types/primitives/unix_time_now.rs +++ b/src/eth/types/primitives/unix_time_now.rs @@ -1,5 +1,6 @@ use display_json::DebugAsJson; +use crate::eth::storage::permanent::rocks::types::UnixTimeRocksdb; use crate::eth::types::UnixTime; /// [`UnixTime`] that automatically sets the current time when created. @@ -18,3 +19,9 @@ impl From for UnixTimeNow { Self(value) } } + +impl From for UnixTimeNow { + fn from(value: UnixTimeRocksdb) -> Self { + Self(value.into()) + } +} diff --git a/src/infra/metrics/metrics_definitions.rs b/src/infra/metrics/metrics_definitions.rs index 6ba82a8c6..e9ef1fd07 100644 --- a/src/infra/metrics/metrics_definitions.rs +++ b/src/infra/metrics/metrics_definitions.rs @@ -24,12 +24,6 @@ metrics! { metrics! { group: storage_read, - "Time executing storage read_pending_block_number operation." - histogram_duration storage_read_pending_block_number{storage, success}, - - "Time executing storage read_mined_block_number operation." - histogram_duration storage_read_mined_block_number{storage, success}, - "Time executing storage read_account operation." histogram_duration storage_read_account{storage, point_in_time, hit}, From 5617bc94132a9ae1c385ea3ee831af483e73af45 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Thu, 3 Sep 2026 13:09:58 -0300 Subject: [PATCH 26/31] forward access list config --- src/eth/follower/consensus.rs | 16 ++++++++++++---- .../importer/{importer_config.rs => config.rs} | 9 +++++++-- src/eth/follower/importer/mod.rs | 10 +++++----- .../{importer_supervisor.rs => supervisor.rs} | 5 +++++ src/eth/rpc/server.rs | 1 + 5 files changed, 30 insertions(+), 11 deletions(-) rename src/eth/follower/importer/{importer_config.rs => config.rs} (93%) rename src/eth/follower/importer/{importer_supervisor.rs => supervisor.rs} (98%) diff --git a/src/eth/follower/consensus.rs b/src/eth/follower/consensus.rs index 9dbdbe968..e9842d7f4 100644 --- a/src/eth/follower/consensus.rs +++ b/src/eth/follower/consensus.rs @@ -61,6 +61,9 @@ pub trait Consensus: Send + Sync { !(lag.is_far_behind() || lag.is_ahead()) } + /// Whether transactions forwarded to the leader should carry a pre-computed access list. + fn forward_access_list(&self) -> bool; + /// Forwards a transaction to leader. /// /// The current machine name is sent as the `x-client` header by `BlockchainClient`, so the leader @@ -71,11 +74,16 @@ pub trait Consensus: Send + Sync { tracing::info!(%tx_hash, "forwarding transaction to leader"); - let access_list = self // make this configurable (?) - .get_executor() - .execute_local_call::(tx.into(), ExecutionKind::AccessList)?; + let access_list = if self.forward_access_list() { + Some( + self.get_executor() + .execute_local_call::(tx.into(), ExecutionKind::AccessList)?, + ) + } else { + None + }; - let hash = self.get_client().send_raw_transaction_to_leader(tx_data.into(), Some(access_list)).await?; + let hash = self.get_client().send_raw_transaction_to_leader(tx_data.into(), access_list).await?; #[cfg(feature = "metrics")] metrics::inc_consensus_forward(start.elapsed()); diff --git a/src/eth/follower/importer/importer_config.rs b/src/eth/follower/importer/config.rs similarity index 93% rename from src/eth/follower/importer/importer_config.rs rename to src/eth/follower/importer/config.rs index 4807eddc9..66fb9e67f 100644 --- a/src/eth/follower/importer/importer_config.rs +++ b/src/eth/follower/importer/config.rs @@ -12,8 +12,8 @@ use crate::eth::follower::ConsensusError; use crate::eth::follower::ImporterError; use crate::eth::follower::importer::BlockchainClient; use crate::eth::follower::importer::ImporterMode; -use crate::eth::follower::importer::importer_supervisor::ImporterConsensus; -use crate::eth::follower::importer::importer_supervisor::start_importer; +use crate::eth::follower::importer::supervisor::ImporterConsensus; +use crate::eth::follower::importer::supervisor::start_importer; use crate::eth::miner::Miner; use crate::eth::rpc::RpcContext; use crate::eth::storage::StratusStorage; @@ -47,6 +47,10 @@ pub struct ImporterConfig { #[arg(long = "enable-block-changes-replication", env = "ENABLE_BLOCK_CHANGES_REPLICATION", default_value = "false")] pub enable_block_changes_replication: bool, + /// Compute an access list for transactions before forwarding them to the leader. + #[arg(long = "forward-access-list", env = "FORWARD_ACCESS_LIST", default_value = "true", required = false)] + pub forward_access_list: bool, + /// Specify the block to stop importing. (useful for validating a follower db against a fake leader) #[arg(long = "stop-at-block", env = "STOP_AT_BLOCK")] pub stop_at_block: Option, @@ -95,6 +99,7 @@ impl ImporterConfig { storage: Arc::clone(&storage), chain: Arc::clone(&chain), executor: Arc::clone(&executor), + forward_access_list: self.forward_access_list, }); spawn( diff --git a/src/eth/follower/importer/mod.rs b/src/eth/follower/importer/mod.rs index 598c23aa6..92bb07db4 100644 --- a/src/eth/follower/importer/mod.rs +++ b/src/eth/follower/importer/mod.rs @@ -1,8 +1,8 @@ +pub(crate) mod config; mod fetchers; -pub(crate) mod importer_config; -#[allow(clippy::module_inception)] -mod importer_supervisor; mod importers; +#[allow(clippy::module_inception)] +mod supervisor; use std::borrow::Cow; use std::sync::Arc; use std::sync::atomic::AtomicU64; @@ -10,9 +10,9 @@ use std::sync::atomic::Ordering; use std::time::Duration; use anyhow::bail; -pub use importer_config::ImporterConfig; -pub use importer_supervisor::ImporterConsensus; +pub use config::ImporterConfig; pub use importers::BlockchainClient; +pub use supervisor::ImporterConsensus; use tokio::sync::mpsc; use tokio::time::timeout; use tracing::Span; diff --git a/src/eth/follower/importer/importer_supervisor.rs b/src/eth/follower/importer/supervisor.rs similarity index 98% rename from src/eth/follower/importer/importer_supervisor.rs rename to src/eth/follower/importer/supervisor.rs index b78df75cc..cf029834d 100644 --- a/src/eth/follower/importer/importer_supervisor.rs +++ b/src/eth/follower/importer/supervisor.rs @@ -152,9 +152,14 @@ pub struct ImporterConsensus { pub storage: Arc, pub chain: Arc, pub executor: Arc, + pub forward_access_list: bool, } impl Consensus for ImporterConsensus { + fn forward_access_list(&self) -> bool { + self.forward_access_list + } + async fn lag(&self) -> anyhow::Result { let last_fetched_time = LATEST_FETCHED_BLOCK_TIME.load(Ordering::Relaxed); diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index 6411d8cf8..05150a368 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -630,6 +630,7 @@ async fn stratus_init_importer(params: Params<'_>, ctx: Arc, ext: Ex enable_block_changes_replication: std::env::var("ENABLE_BLOCK_CHANGES_REPLICATION") .ok() .is_some_and(|val| val == "1" || val == "true"), + forward_access_list: !matches!(std::env::var("FORWARD_ACCESS_LIST").as_deref(), Ok("0") | Ok("false")), stop_at_block: None, }; From e3514741b1093ad1acb3002a2fb3b471abbdc2c3 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Thu, 3 Sep 2026 15:39:51 -0300 Subject: [PATCH 27/31] fix tests --- src/eth/storage/stratus_storage.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index aa76d00a0..a6256406a 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -277,15 +277,13 @@ impl StratusStorage { cache: StorageCache, #[cfg(feature = "dev")] perm_config: crate::eth::storage::permanent::PermanentStorageConfig, ) -> Result { - let latest_block = perm - .read_block(BlockFilter::Latest)? - .ok_or(StorageError::BlockNotFound { filter: BlockFilter::Latest })?; + let latest_block_info = perm.read_block(BlockFilter::Latest)?.map(|block| block.header.into()).unwrap_or_default(); let this = Self { temp, cache, perm, - latest_state_lock: LatestStateLock::new(latest_block.header.into()), + latest_state_lock: LatestStateLock::new(latest_block_info), #[cfg(feature = "dev")] perm_config, }; From dab55706c1fb232c192755aad7283dbb575058a7 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Thu, 3 Sep 2026 16:45:34 -0300 Subject: [PATCH 28/31] review --- src/eth/rpc/middleware/rpc_middleware.rs | 18 +++++++++++++----- src/eth/storage/resolve_pending.rs | 3 +-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/eth/rpc/middleware/rpc_middleware.rs b/src/eth/rpc/middleware/rpc_middleware.rs index bea8be6c0..4f2cadf62 100644 --- a/src/eth/rpc/middleware/rpc_middleware.rs +++ b/src/eth/rpc/middleware/rpc_middleware.rs @@ -177,6 +177,17 @@ impl RpcServiceT for RpcMiddleware { metrics::set_rpc_requests_active(active as u64); } + if let Some(future_response) = reject_client(&client, request_id.clone()) { + return RpcResponse { + client, + id: request_id.to_string(), + method: method.to_string(), + tx: None, + start: Instant::now(), + future_response, + }; + } + let span = info_span!( parent: None, "rpc::request", @@ -203,7 +214,7 @@ impl RpcServiceT for RpcMiddleware { s.rec_str("rpc_method", &method); }); - let (future, tracing_identifiers) = if method == "eth_sendRawTransaction" { + let (future_response, tracing_identifiers) = if method == "eth_sendRawTransaction" { drop(middleware_enter); match eth_send_raw_transaction(request, Arc::clone(&self.ctx), span) { Ok(result) => result, @@ -264,12 +275,9 @@ impl RpcServiceT for RpcMiddleware { "rpc request" ); - let id = request_id.to_string(); - - let future_response = reject_client(&client, request_id.clone()).unwrap_or(future); RpcResponse { client, - id, + id: request_id.to_string(), method: method.to_string(), tx: tracing_identifiers, start: Instant::now(), diff --git a/src/eth/storage/resolve_pending.rs b/src/eth/storage/resolve_pending.rs index cc9d7c85b..54f98df30 100644 --- a/src/eth/storage/resolve_pending.rs +++ b/src/eth/storage/resolve_pending.rs @@ -143,8 +143,7 @@ mod tests { assert_eq!(*number, call_block, "stale Full call should downgrade to MinedPast(block_number), not prev()"); } other => panic!("expected Past, got {other:?}"), - } - assert!(matches!(point, super::MinedPointInTime::Latest(_, None)), "no guard for historical read"); + }; } other => panic!("expected Miss, got {other:?}"), } From 68636553bb7bf530dd32b725b0998671bdf44d0b Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Thu, 3 Sep 2026 17:01:15 -0300 Subject: [PATCH 29/31] remove recursion --- src/eth/storage/stratus_storage.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index a6256406a..822c7db68 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -601,12 +601,18 @@ impl StratusStorage { pub fn read_block_info(&self, filter: BlockFilter) -> Result, StorageError> { let latest_state = self.latest_state_lock.read(); - match filter { + let mined = latest_state.number; + + let reduced_filter = match filter { + BlockFilter::Number(n) if n == mined.next_block_number() => BlockFilter::Pending, + BlockFilter::Number(n) if n == mined => BlockFilter::Latest, + filter => filter, + }; + + match reduced_filter { BlockFilter::Pending => Ok(Some(self.read_pending_block_header())), BlockFilter::Latest => Ok(Some(*latest_state)), - BlockFilter::Number(number) if number == self.read_mined_block_number() => self.read_block_info(BlockFilter::Latest), - BlockFilter::Number(number) if number == self.read_mined_block_number().next_block_number() => self.read_block_info(BlockFilter::Pending), - _ => { + filter => { drop(latest_state); Ok(self.read_block(filter)?.map(|block| block.header.into())) } From fcd655fac1b364d38e9c5e3274e34316e153f4c9 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Thu, 3 Sep 2026 17:08:46 -0300 Subject: [PATCH 30/31] remove unwraps --- src/eth/storage/stratus_storage.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/eth/storage/stratus_storage.rs b/src/eth/storage/stratus_storage.rs index 822c7db68..7c18f6ee6 100644 --- a/src/eth/storage/stratus_storage.rs +++ b/src/eth/storage/stratus_storage.rs @@ -887,20 +887,35 @@ impl StratusStorage { } } - fn load_slots_to_cache(&self, slots: Vec<(Address, SlotIndex)>) { - let existing_slots: HashMap<(Address, SlotIndex), SlotValue> = self.perm.read_slots(slots.clone()).unwrap().into_iter().collect(); //unwrap + fn load_slots_to_cache(&self, slots: Vec<(Address, SlotIndex)>) -> Result<(), StorageError> { + let existing_slots: HashMap<(Address, SlotIndex), SlotValue> = self + .perm + .read_slots(slots.clone()) + .inspect_err(|err| tracing::error!(?err, "reading slots from perm failed"))? + .into_iter() + .collect(); + for (address, index) in slots { let value = existing_slots.get(&(address, index)).copied().unwrap_or_default(); Slot::cache_latest_if_missing(self, (address, index), Slot { index, value }); } + + Ok(()) } - fn load_accounts_to_cache(&self, addresses: Vec
) { - let existing_accounts: HashMap = self.perm.read_accounts(addresses.clone()).unwrap().into_iter().collect(); //unwrap + fn load_accounts_to_cache(&self, addresses: Vec
) -> Result<(), StorageError> { + let existing_accounts: HashMap = self + .perm + .read_accounts(addresses.clone()) + .inspect_err(|err| tracing::error!(?err, "reading accounts from perm failed"))? + .into_iter() + .collect(); for address in addresses { let account = existing_accounts.get(&address).cloned().unwrap_or_default(); Account::cache_latest_if_missing(self, address, account); } + + Ok(()) } pub fn load_access_list(&self, access_list: AccessListOutput) { @@ -915,8 +930,8 @@ impl StratusStorage { } // skip each step if prev empty Account::retain_missing_keys(self, &mut account_addresses); Slot::retain_missing_keys(self, &mut slot_keys); - self.load_accounts_to_cache(account_addresses); - self.load_slots_to_cache(slot_keys); + self.load_accounts_to_cache(account_addresses).ok(); + self.load_slots_to_cache(slot_keys).ok(); } } From b79faa76de9830f627d42c07336e12cd2dff7395 Mon Sep 17 00:00:00 2001 From: Daniel Freire Date: Fri, 4 Sep 2026 01:22:04 -0300 Subject: [PATCH 31/31] fix test --- e2e/test/automine/e2e-json-rpc.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/e2e/test/automine/e2e-json-rpc.test.ts b/e2e/test/automine/e2e-json-rpc.test.ts index 8f1ad7865..11823f505 100644 --- a/e2e/test/automine/e2e-json-rpc.test.ts +++ b/e2e/test/automine/e2e-json-rpc.test.ts @@ -104,8 +104,10 @@ describe("JSON-RPC", () => { return; } - await send("stratus_disableUnknownClients"); - await send("stratus_enableUnknownClients"); + // client identification is required to toggle unknown clients once they are disabled + const identifiedHeaders = { "x-app": "test-client" }; + await send("stratus_disableUnknownClients", [], identifiedHeaders); + await send("stratus_enableUnknownClients", [], identifiedHeaders); // Request without client identification should now succeed const blockNumber = await send("eth_blockNumber");