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"); diff --git a/src/eth/executor/evm/types/input/call_execution.rs b/src/eth/executor/evm/types/input/call_execution.rs index 2c1cf002e..5b751fef3 100644 --- a/src/eth/executor/evm/types/input/call_execution.rs +++ b/src/eth/executor/evm/types/input/call_execution.rs @@ -8,12 +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::PointInTime; use crate::eth::types::UnixTime; use crate::eth::types::Wei; use crate::ext::OptionExt; @@ -61,32 +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) -> 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: ExecutionKind::CallLatest(block.number.prev().unwrap_or_default()), - } - } - - /// 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), - }; - 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/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 e21d8a87e..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; @@ -61,6 +60,7 @@ 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 @@ -70,6 +70,7 @@ use crate::infra::tracing::SpanExt; #[derive(Default)] pub struct ExecutorLocks { transaction: Mutex<()>, + transaction_warmup: Semaphore, } pub struct Executor { @@ -95,7 +96,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, @@ -172,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() { @@ -273,7 +277,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")] @@ -289,6 +293,12 @@ 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); + } + // execute according to the strategy const INFINITE_ATTEMPTS: usize = usize::MAX; @@ -296,13 +306,12 @@ 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(); + let start = metrics::now(); #[cfg(feature = "metrics")] - let lock_wait = lock_wait_start.elapsed(); - #[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); @@ -311,9 +320,8 @@ impl Executor { let execution_elapsed = start.elapsed(); drop(transaction_lock); + drop(permit); - #[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); @@ -348,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!( @@ -407,15 +415,14 @@ impl Executor { } } - /// Executes a transaction without persisting state changes. + /// Executes a read-only call in the local EVM, without persisting state changes. #[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, kind: ExecutionKind) -> Result where Output: TryFrom, { #[cfg(feature = "metrics")] let start = metrics::now(); - Span::with(|s| { s.rec_opt("from", &call_input.from); s.rec_opt("to", &call_input.to); @@ -425,31 +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 point_in_time { - PointInTime::Pending => { - let pending_header = self.storage.read_pending_block_header(); - CallExecutionInput::from_pending_block(call_input, pending_header) - } - _ => { - let Some(block) = self.storage.read_block(point_in_time.into())? else { - return Err(RpcError::BlockFilterInvalid { filter: point_in_time.into() }.into()); - }; - CallExecutionInput::from_mined_block(call_input, block.header, point_in_time) - } + 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 { + 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/consensus.rs b/src/eth/follower/consensus.rs index 469ac9011..e9842d7f4 100644 --- a/src/eth/follower/consensus.rs +++ b/src/eth/follower/consensus.rs @@ -2,10 +2,14 @@ use std::sync::Arc; use strum::AsRefStr; +use crate::eth::executor::AccessListOutput; +use crate::eth::executor::Executor; use crate::eth::follower::importer::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; #[cfg(feature = "metrics")] use crate::infra::metrics; @@ -57,17 +61,29 @@ 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 /// attributes the transaction to this node automatically. - async fn forward_to_leader(&self, tx_hash: Hash, tx_data: Bytes) -> Result { + async fn forward_to_leader(&self, tx: TransactionInput, tx_hash: Hash, tx_data: Bytes) -> Result { #[cfg(feature = "metrics")] let start = metrics::now(); tracing::info!(%tx_hash, "forwarding transaction to leader"); - let hash = self.get_chain()?.send_raw_transaction_to_leader(tx_data.into()).await?; + 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(), access_list).await?; #[cfg(feature = "metrics")] metrics::inc_consensus_forward(start.elapsed()); @@ -75,7 +91,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/config.rs similarity index 92% rename from src/eth/follower/importer/importer_config.rs rename to src/eth/follower/importer/config.rs index 297948dc8..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, @@ -94,6 +98,8 @@ impl ImporterConfig { let consensus = Arc::new(ImporterConsensus { 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/importers/blockchain_client.rs b/src/eth/follower/importer/importers/blockchain_client.rs index 1b654d113..905477e0c 100644 --- a/src/eth/follower/importer/importers/blockchain_client.rs +++ b/src/eth/follower/importer/importers/blockchain_client.rs @@ -20,6 +20,7 @@ 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::pagination; use crate::eth::storage::permanent::rocks::types::BlockChangesRocksdb; @@ -343,11 +344,12 @@ impl BlockchainClient { /// /// The current machine name is sent as the `x-client` header on every request (see `client_headers`), /// so the leader attributes the transaction to this node automatically. - pub async fn send_raw_transaction_to_leader(&self, tx: AlloyBytes) -> 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 result = self.http.request::("eth_sendRawTransaction", [tx]).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/follower/importer/importers/fake_leader.rs b/src/eth/follower/importer/importers/fake_leader.rs index 4a6133295..5c88e7df1 100644 --- a/src/eth/follower/importer/importers/fake_leader.rs +++ b/src/eth/follower/importer/importers/fake_leader.rs @@ -36,7 +36,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/follower/importer/mod.rs b/src/eth/follower/importer/mod.rs index e4b1d7a04..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; @@ -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/follower/importer/importer_supervisor.rs b/src/eth/follower/importer/supervisor.rs similarity index 95% rename from src/eth/follower/importer/importer_supervisor.rs rename to src/eth/follower/importer/supervisor.rs index 2d5acef22..cf029834d 100644 --- a/src/eth/follower/importer/importer_supervisor.rs +++ b/src/eth/follower/importer/supervisor.rs @@ -151,9 +151,15 @@ pub async fn start_importer( 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); @@ -183,7 +189,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/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 74d4a7e90..4f2cadf62 100644 --- a/src/eth/rpc/middleware/rpc_middleware.rs +++ b/src/eth/rpc/middleware/rpc_middleware.rs @@ -30,13 +30,13 @@ use crate::eth::codegen; use crate::eth::codegen::ContractName; use crate::eth::codegen::SoliditySignature; 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; @@ -61,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, + } } } @@ -162,13 +166,35 @@ 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); + } + + 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", 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, @@ -181,60 +207,62 @@ 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((_, tx_data)) = tx_data_result { - 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); + 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, + 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 { - 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(), @@ -247,37 +275,11 @@ 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 future_response = reject_client(&client, request.id.clone()).unwrap_or(Box::pin(self.service.call(request))); RpcResponse { client, - id, + id: request_id.to_string(), method: method.to_string(), - tx, + tx: tracing_identifiers, start: Instant::now(), future_response, } @@ -425,8 +427,7 @@ impl Future for RpcResponse<'_> { // Helpers // ----------------------------------------------------------------------------- -struct TransactionTracingIdentifiers { - pub client: Option, +pub struct TransactionTracingIdentifiers { pub hash: Option, pub contract: ContractName, pub function: SoliditySignature, @@ -438,16 +439,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), }) } @@ -455,7 +455,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), @@ -470,7 +469,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 231de6dd4..dd319a190 100644 --- a/src/eth/rpc/mod.rs +++ b/src/eth/rpc/mod.rs @@ -15,7 +15,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 d03dbd60c..05150a368 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; @@ -68,11 +74,13 @@ 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::pagination; 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; @@ -183,19 +191,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"), @@ -350,7 +358,6 @@ fn register_methods(mut module: RpcModule) -> anyhow::Result, 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, }; @@ -1146,7 +1154,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) { + + 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,12 +1198,16 @@ fn rpc_call(params: Params<'_>, ctx: Arc) -> Result, ctx: Arc, ext: Extensions) -> Result { @@ -1298,13 +1317,63 @@ fn stratus_access_list(params: Params<'_>, ctx: Arc, ext: Extensions ctx.server .executor - .execute_local_call::(call, PointInTime::Latest) + .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")) } -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), 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_or_default::>(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!( @@ -1316,17 +1385,6 @@ 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()), - _ => { - 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 @@ -1352,7 +1410,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"); @@ -1360,7 +1418,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)) { + 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), }, diff --git a/src/eth/storage/cache.rs b/src/eth/storage/cache.rs index a5af99168..3f411a3e1 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; @@ -85,12 +86,42 @@ 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 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) + } + + 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`. +#[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 } } diff --git a/src/eth/storage/permanent/rocks/rocks_permanent.rs b/src/eth/storage/permanent/rocks/rocks_permanent.rs index 6817156d6..76734e227 100644 --- a/src/eth/storage/permanent/rocks/rocks_permanent.rs +++ b/src/eth/storage/permanent/rocks/rocks_permanent.rs @@ -29,6 +29,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; @@ -139,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 { @@ -177,6 +178,11 @@ 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 }) + } + 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 982f8e1ed..c6fd70e50 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; @@ -347,6 +348,15 @@ impl RocksStorageState { } } + pub fn read_slots(&self, slot_keys: Vec<(Address, SlotIndex)>) -> Result> { + 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> { if address.is_coinbase() || address.is_zero() { return Ok(None); @@ -381,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> { 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 1909558fa..54f98df30 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) @@ -106,7 +87,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), } } } @@ -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,15 +136,14 @@ 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) => { assert_eq!(*number, call_block, "stale Full call should downgrade to MinedPast(block_number), not prev()"); } other => panic!("expected Past, got {other:?}"), - } - assert!(point.take_guard().is_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 3555c8322..7c18f6ee6 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::State; use crate::eth::executor::TransactionExecution; use crate::eth::executor::types::state::AccountOriginalsReader; @@ -20,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; @@ -29,11 +33,9 @@ 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; -#[cfg(feature = "dev")] use crate::eth::types::SlotValue; use crate::eth::types::TransactionStage; use crate::eth::types::UnixTime; @@ -52,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. @@ -59,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, } @@ -91,7 +135,11 @@ 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; + /// 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. + 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. @@ -112,7 +160,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"); @@ -121,6 +169,15 @@ 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)); + } + 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| { @@ -162,7 +219,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() { @@ -172,6 +229,16 @@ 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)); + } + 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"); @@ -210,11 +277,13 @@ impl StratusStorage { cache: StorageCache, #[cfg(feature = "dev")] perm_config: crate::eth::storage::permanent::PermanentStorageConfig, ) -> Result { + let latest_block_info = perm.read_block(BlockFilter::Latest)?.map(|block| block.header.into()).unwrap_or_default(); + let this = Self { temp, cache, perm, - transient_state_lock: parking_lot::RwLock::new(()), + latest_state_lock: LatestStateLock::new(latest_block_info), #[cfg(feature = "dev")] perm_config, }; @@ -294,24 +363,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) { @@ -336,6 +393,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(); @@ -355,7 +413,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), @@ -364,7 +422,13 @@ 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) { + //bench without try_read + 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 @@ -377,17 +441,14 @@ 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) => { - E::cache_latest_if_missing(self, key, value.clone()); - } - // Cache / Historical / (Mined, Temp): nothing to cache. - _ => {} + // 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) } @@ -505,8 +566,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(()) @@ -536,6 +599,26 @@ impl StratusStorage { }) } + pub fn read_block_info(&self, filter: BlockFilter) -> Result, StorageError> { + let latest_state = self.latest_state_lock.read(); + 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)), + filter => { + 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(); @@ -782,13 +865,74 @@ 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(_) => 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 }), } } + + 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(_) => 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)>) -> 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
) -> 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) { + // can error + 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)); + } + } // 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).ok(); + self.load_slots_to_cache(slot_keys).ok(); + } } #[cfg(test)] @@ -805,10 +949,10 @@ mod tests { 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); + let evm_input = TransactionExecutionInput::create(&TransactionInput::default(), header); let result = TransactionExecutionResult { result: ExecutionResult::Success, @@ -817,6 +961,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"); 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 94a4c7194..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), @@ -178,6 +177,36 @@ impl InmemoryTransactionTemporaryStorage { } } + /// 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)); + } + } + + /// 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)); + } + } + // ------------------------------------------------------------------------- // Direct state manipulation (for testing) // ------------------------------------------------------------------------- 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 fed07edbe..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), @@ -9,12 +12,20 @@ pub enum ExecutionKind { #[default] Transaction, RPC(PointInTime), + AccessList, } 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 { @@ -23,7 +34,17 @@ impl From<&ExecutionKind> for PointInTime { ExecutionKind::RPC(pit) => *pit, ExecutionKind::Transaction => PointInTime::Pending, ExecutionKind::CallPast(number) => PointInTime::Past(*number), - ExecutionKind::CallLatest(_) => PointInTime::Latest, + ExecutionKind::CallLatest(_) | ExecutionKind::AccessList => PointInTime::Latest, + } + } +} + +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/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, + } + } +} diff --git a/src/infra/metrics/metrics_definitions.rs b/src/infra/metrics/metrics_definitions.rs index 77a912934..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}, @@ -135,8 +129,11 @@ 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{}, + + "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}, 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(); + } +}