diff --git a/Cargo.lock b/Cargo.lock index 961ce3ef479..e0fa3c0ad87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12506,6 +12506,7 @@ dependencies = [ "cairo-lang-runner", "cairo-lang-starknet-classes", "cairo-lang-utils", + "dashmap", "derive_more 2.1.1", "expect-test", "flate2", diff --git a/crates/blockifier/src/blockifier/transaction_executor.rs b/crates/blockifier/src/blockifier/transaction_executor.rs index 786dfc5e56c..ce91677373e 100644 --- a/crates/blockifier/src/blockifier/transaction_executor.rs +++ b/crates/blockifier/src/blockifier/transaction_executor.rs @@ -15,6 +15,7 @@ use crate::bouncer::{Bouncer, BouncerWeights, CasmHashComputationData}; use crate::concurrency::worker_logic::WorkerExecutor; use crate::concurrency::worker_pool::WorkerPool; use crate::context::BlockContext; +use crate::metrics::{record_transaction_executor_metrics, TransactionExecutorMetrics}; use crate::state::cached_state::{CachedState, CommitmentStateDiff, StateMaps, TransactionalState}; use crate::state::compiled_class_hash_migration::CompiledClassHashMigrationUpdater; use crate::state::errors::StateError; @@ -187,6 +188,7 @@ impl TransactionExecutor { execution_deadline: Option, ) -> Vec> { let mut results = Vec::new(); + let mut execution_attempts = 0; for tx in txs { if let Some(deadline) = execution_deadline { if Instant::now() > deadline { @@ -194,6 +196,7 @@ impl TransactionExecutor { break; } } + execution_attempts += 1; match self.execute(tx) { Ok((tx_execution_info, state_diff)) => { results.push(Ok((tx_execution_info, state_diff))) @@ -202,6 +205,13 @@ impl TransactionExecutor { Err(error) => results.push(Err(error)), } } + record_transaction_executor_metrics(TransactionExecutorMetrics { + transactions: u64::try_from(txs.len()).expect("transaction count should fit in u64"), + committed_transactions: u64::try_from(results.len()) + .expect("committed transaction count should fit in u64"), + execution_attempts, + ..Default::default() + }); results } diff --git a/crates/blockifier/src/concurrency/worker_logic.rs b/crates/blockifier/src/concurrency/worker_logic.rs index 6c20f5083b7..ba888c481a0 100644 --- a/crates/blockifier/src/concurrency/worker_logic.rs +++ b/crates/blockifier/src/concurrency/worker_logic.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::fmt::Debug; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; @@ -23,7 +23,12 @@ use crate::concurrency::versioned_state::{ }; use crate::concurrency::TxIndex; use crate::context::BlockContext; -use crate::metrics::{CALLS_RUNNING_NATIVE, TOTAL_CALLS}; +use crate::metrics::{ + record_transaction_executor_metrics, + TransactionExecutorMetrics, + CALLS_RUNNING_NATIVE, + TOTAL_CALLS, +}; use crate::state::cached_state::{ContractClassMapping, StateMaps, TransactionalState}; use crate::state::state_api::{StateReader, UpdatableState}; use crate::transaction::objects::{TransactionExecutionInfo, TransactionExecutionResult}; @@ -47,10 +52,10 @@ pub struct ExecutionTaskOutput { #[derive(Default)] pub struct ConcurrencyMetrics { - abort_counter: AtomicUsize, - abort_in_commit_counter: AtomicUsize, - execute_counter: AtomicUsize, - validate_counter: AtomicUsize, + abort_counter: AtomicU64, + abort_in_commit_counter: AtomicU64, + execute_counter: AtomicU64, + validate_counter: AtomicU64, } impl ConcurrencyMetrics { @@ -66,13 +71,14 @@ impl ConcurrencyMetrics { pub fn count_validate(&self) { self.validate_counter.fetch_add(1, Ordering::Relaxed); } - pub fn get_metrics(&self) -> (usize, usize, usize, usize) { - ( - self.abort_counter.load(Ordering::Relaxed), - self.abort_in_commit_counter.load(Ordering::Relaxed), - self.execute_counter.load(Ordering::Relaxed), - self.validate_counter.load(Ordering::Relaxed), - ) + pub fn snapshot(&self) -> TransactionExecutorMetrics { + TransactionExecutorMetrics { + execution_attempts: self.execute_counter.load(Ordering::Relaxed), + validation_attempts: self.validate_counter.load(Ordering::Relaxed), + aborts: self.abort_counter.load(Ordering::Relaxed), + commit_phase_aborts: self.abort_in_commit_counter.load(Ordering::Relaxed), + ..Default::default() + } } } @@ -419,14 +425,22 @@ impl WorkerExecutor { impl WorkerExecutor { pub fn commit_chunk_and_recover_block_state(&self, n_committed_txs: usize) -> U { - let (abort_counter, abort_in_commit_counter, execute_counter, validate_counter) = - self.metrics.get_metrics(); let n_txs = self.get_n_txs(); + let metrics = TransactionExecutorMetrics { + transactions: u64::try_from(n_txs).expect("transaction count should fit in u64"), + committed_transactions: u64::try_from(n_committed_txs) + .expect("committed transaction count should fit in u64"), + ..self.metrics.snapshot() + }; + record_transaction_executor_metrics(metrics); log::debug!( "Concurrent execution done. Number of transactions: {n_txs}; Committed chunk size: \ - {n_committed_txs}; Execute counter: {execute_counter}; Validate counter: \ - {validate_counter}; Abort counter: {abort_counter}; Abort in commit counter: \ - {abort_in_commit_counter}" + {n_committed_txs}; Execute counter: {}; Validate counter: {}; Abort counter: {}; Abort \ + in commit counter: {}", + metrics.execution_attempts, + metrics.validation_attempts, + metrics.aborts, + metrics.commit_phase_aborts, ); self.state.into_inner_state().commit_chunk_and_recover_block_state(n_committed_txs) diff --git a/crates/blockifier/src/concurrency/worker_logic_test.rs b/crates/blockifier/src/concurrency/worker_logic_test.rs index 2e4ab6fae32..432f96d84ef 100644 --- a/crates/blockifier/src/concurrency/worker_logic_test.rs +++ b/crates/blockifier/src/concurrency/worker_logic_test.rs @@ -16,7 +16,7 @@ use starknet_api::transaction::TransactionVersion; use starknet_api::{contract_address, declare_tx_args, felt, invoke_tx_args, nonce, storage_key}; use starknet_types_core::felt::Felt; -use super::WorkerExecutor; +use super::{ConcurrencyMetrics, WorkerExecutor}; use crate::bouncer::Bouncer; use crate::concurrency::fee_utils::STORAGE_READ_SEQUENCER_BALANCE_INDICES; use crate::concurrency::scheduler::{Task, TransactionStatus}; @@ -881,3 +881,19 @@ fn test_worker_commit_phase_with_halt() { assert!(!result.unwrap().is_reverted()); } } + +#[test] +fn concurrency_metrics_snapshot_has_named_counters() { + let metrics = ConcurrencyMetrics::default(); + metrics.count_execute(); + metrics.count_execute(); + metrics.count_validate(); + metrics.count_abort(); + metrics.count_abort_in_commit(); + + let snapshot = metrics.snapshot(); + assert_eq!(snapshot.execution_attempts, 2); + assert_eq!(snapshot.validation_attempts, 1); + assert_eq!(snapshot.aborts, 1); + assert_eq!(snapshot.commit_phase_aborts, 1); +} diff --git a/crates/blockifier/src/metrics.rs b/crates/blockifier/src/metrics.rs index 7c75e726aa1..982c9e7d816 100644 --- a/crates/blockifier/src/metrics.rs +++ b/crates/blockifier/src/metrics.rs @@ -1,5 +1,6 @@ use apollo_metrics::define_metrics; use apollo_metrics::metrics::{MetricCounter, MetricDetails, MetricScope}; +use std::sync::atomic::{AtomicU64, Ordering}; define_metrics!( Blockifier => { @@ -29,6 +30,49 @@ define_metrics!( pub const BLOCKIFIER_METRIC_RATE_DURATION: &str = "5m"; +/// Process-lifetime transaction execution counters. +/// +/// These mirror the per-chunk counters emitted by Blockifier's concurrent +/// executor while also covering sequential `execute_txs` calls. They are kept +/// as relaxed atomics so downstream nodes can export them without adding a +/// metrics dependency to the execution hot path. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TransactionExecutorMetrics { + pub transactions: u64, + pub committed_transactions: u64, + pub execution_attempts: u64, + pub validation_attempts: u64, + pub aborts: u64, + pub commit_phase_aborts: u64, +} + +static TRANSACTIONS: AtomicU64 = AtomicU64::new(0); +static COMMITTED_TRANSACTIONS: AtomicU64 = AtomicU64::new(0); +static EXECUTION_ATTEMPTS: AtomicU64 = AtomicU64::new(0); +static VALIDATION_ATTEMPTS: AtomicU64 = AtomicU64::new(0); +static ABORTS: AtomicU64 = AtomicU64::new(0); +static COMMIT_PHASE_ABORTS: AtomicU64 = AtomicU64::new(0); + +pub fn transaction_executor_metrics() -> TransactionExecutorMetrics { + TransactionExecutorMetrics { + transactions: TRANSACTIONS.load(Ordering::Relaxed), + committed_transactions: COMMITTED_TRANSACTIONS.load(Ordering::Relaxed), + execution_attempts: EXECUTION_ATTEMPTS.load(Ordering::Relaxed), + validation_attempts: VALIDATION_ATTEMPTS.load(Ordering::Relaxed), + aborts: ABORTS.load(Ordering::Relaxed), + commit_phase_aborts: COMMIT_PHASE_ABORTS.load(Ordering::Relaxed), + } +} + +pub(crate) fn record_transaction_executor_metrics(metrics: TransactionExecutorMetrics) { + TRANSACTIONS.fetch_add(metrics.transactions, Ordering::Relaxed); + COMMITTED_TRANSACTIONS.fetch_add(metrics.committed_transactions, Ordering::Relaxed); + EXECUTION_ATTEMPTS.fetch_add(metrics.execution_attempts, Ordering::Relaxed); + VALIDATION_ATTEMPTS.fetch_add(metrics.validation_attempts, Ordering::Relaxed); + ABORTS.fetch_add(metrics.aborts, Ordering::Relaxed); + COMMIT_PHASE_ABORTS.fetch_add(metrics.commit_phase_aborts, Ordering::Relaxed); +} + pub struct CacheMetrics { misses: MetricCounter, hits: MetricCounter, @@ -48,6 +92,43 @@ impl CacheMetrics { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn transaction_executor_metrics_accumulate_monotonically() { + let before = transaction_executor_metrics(); + let increment = TransactionExecutorMetrics { + transactions: 50, + committed_transactions: 48, + execution_attempts: 73, + validation_attempts: 81, + aborts: 23, + commit_phase_aborts: 4, + }; + + record_transaction_executor_metrics(increment); + let after = transaction_executor_metrics(); + + assert!(after.transactions >= before.transactions + increment.transactions); + assert!( + after.committed_transactions + >= before.committed_transactions + increment.committed_transactions + ); + assert!( + after.execution_attempts >= before.execution_attempts + increment.execution_attempts + ); + assert!( + after.validation_attempts >= before.validation_attempts + increment.validation_attempts + ); + assert!(after.aborts >= before.aborts + increment.aborts); + assert!( + after.commit_phase_aborts >= before.commit_phase_aborts + increment.commit_phase_aborts + ); + } +} + impl CacheMetrics { pub fn register(&self) { self.misses.register(); diff --git a/crates/starknet_api/Cargo.toml b/crates/starknet_api/Cargo.toml index 7c3d88672dc..111c323f8ac 100644 --- a/crates/starknet_api/Cargo.toml +++ b/crates/starknet_api/Cargo.toml @@ -18,6 +18,7 @@ cached.workspace = true cairo-lang-runner.workspace = true cairo-lang-starknet-classes.workspace = true cairo-lang-utils.workspace = true +dashmap.workspace = true derive_more = { workspace = true, features = [ "add", "add_assign", diff --git a/crates/starknet_api/src/abi/abi_utils.rs b/crates/starknet_api/src/abi/abi_utils.rs index 22f9d7ac418..71c5e27b152 100644 --- a/crates/starknet_api/src/abi/abi_utils.rs +++ b/crates/starknet_api/src/abi/abi_utils.rs @@ -3,7 +3,8 @@ use starknet_types_core::felt::{Felt, NonZeroFelt}; use starknet_types_core::hash::{Pedersen, StarkHash}; use crate::abi::constants; -use crate::core::{ContractAddress, EntryPointSelector, PatriciaKey, L2_ADDRESS_UPPER_BOUND}; +use crate::core::{ContractAddress, EntryPointSelector, L2_ADDRESS_UPPER_BOUND, PatriciaKey}; +use crate::hash_cache; use crate::state::StorageKey; #[cfg(test)] @@ -12,13 +13,18 @@ mod test; /// A variant of eth-keccak that computes a value that fits in a Starknet field element. pub fn starknet_keccak(data: &[u8]) -> Felt { + if let Some(cached) = hash_cache::sn_keccak_get(data) { + return cached; + } let mut hasher = Keccak256::new(); hasher.update(data); let mut result: [u8; 32] = hasher.finalize().into(); // Truncate result to 250 bits. *result.first_mut().unwrap() &= 3; - Felt::from_bytes_be(&result) + let result = Felt::from_bytes_be(&result); + hash_cache::sn_keccak_insert(data, result); + result } /// Returns an entry point selector, given its name. @@ -39,8 +45,13 @@ pub fn selector_from_name(entry_point_name: &str) -> EntryPointSelector { pub fn get_storage_var_address(storage_var_name: &str, args: &[Felt]) -> StorageKey { let storage_var_name_hash = starknet_keccak(storage_var_name.as_bytes()); - let storage_key_hash = - args.iter().fold(storage_var_name_hash, |res, arg| Pedersen::hash(&res, arg)); + let storage_key_hash = args.iter().fold(storage_var_name_hash, |res, arg| { + hash_cache::pedersen_pair_get(res, *arg).unwrap_or_else(|| { + let result = Pedersen::hash(&res, arg); + hash_cache::pedersen_pair_insert(res, *arg, result); + result + }) + }); let storage_key = storage_key_hash .mod_floor(&NonZeroFelt::from_raw(Felt::from(*L2_ADDRESS_UPPER_BOUND).to_raw())); diff --git a/crates/starknet_api/src/core.rs b/crates/starknet_api/src/core.rs index ff7017c3983..6b7f892dc3e 100644 --- a/crates/starknet_api/src/core.rs +++ b/crates/starknet_api/src/core.rs @@ -16,9 +16,10 @@ use starknet_types_core::hash::{Pedersen, StarkHash as CoreStarkHash}; use crate::crypto::utils::PublicKey; use crate::hash::{HashOutput, PoseidonHash, StarkHash}; +use crate::hash_cache; use crate::serde_utils::{BytesAsHex, PrefixedBytesAsHex}; use crate::transaction::fields::{Calldata, ContractAddressSalt}; -use crate::{impl_from_through_intermediate, StarknetApiError, StarknetApiResult}; +use crate::{StarknetApiError, StarknetApiResult, impl_from_through_intermediate}; /// Felt. pub fn ascii_as_felt(ascii_str: &str) -> Result { @@ -269,9 +270,14 @@ pub fn calculate_contract_address( constructor_calldata: &Calldata, deployer_address: ContractAddress, ) -> Result { - let constructor_calldata_hash = Pedersen::hash_array(&constructor_calldata.0); + let constructor_calldata_hash = hash_cache::pedersen_array_get(&constructor_calldata.0) + .unwrap_or_else(|| { + let result = Pedersen::hash_array(&constructor_calldata.0); + hash_cache::pedersen_array_insert(&constructor_calldata.0, result); + result + }); let contract_address_prefix = format!("0x{}", hex::encode(CONTRACT_ADDRESS_PREFIX)); - let address = Pedersen::hash_array(&[ + let values = [ Felt::from_hex(contract_address_prefix.as_str()).map_err(|_| { StarknetApiError::OutOfRange { string: contract_address_prefix.clone() } })?, @@ -279,7 +285,12 @@ pub fn calculate_contract_address( salt.0, class_hash.0, constructor_calldata_hash, - ]); + ]; + let address = hash_cache::pedersen_array_get(&values).unwrap_or_else(|| { + let result = Pedersen::hash_array(&values); + hash_cache::pedersen_array_insert(&values, result); + result + }); let (_, address) = address.div_rem(&L2_ADDRESS_UPPER_BOUND); ContractAddress::try_from(address) diff --git a/crates/starknet_api/src/crypto/utils.rs b/crates/starknet_api/src/crypto/utils.rs index 1866418e954..e3db4a618c3 100644 --- a/crates/starknet_api/src/crypto/utils.rs +++ b/crates/starknet_api/src/crypto/utils.rs @@ -11,6 +11,7 @@ use starknet_types_core::hash::{Pedersen, Poseidon, StarkHash as CoreStarkHash}; use thiserror::Error; use crate::hash::StarkHash; +use crate::hash_cache; /// An error that can occur during cryptographic operations. @@ -113,12 +114,20 @@ impl HashChain { // Returns the pedersen hash of the chained felts, hashed with the length of the chain. pub fn get_pedersen_hash(&self) -> StarkHash { - Pedersen::hash_array(self.elements.as_slice()) + hash_cache::pedersen_array_get(self.elements.as_slice()).unwrap_or_else(|| { + let result = Pedersen::hash_array(self.elements.as_slice()); + hash_cache::pedersen_array_insert(self.elements.as_slice(), result); + result + }) } // Returns the poseidon hash of the chained felts. pub fn get_poseidon_hash(&self) -> StarkHash { - Poseidon::hash_array(self.elements.as_slice()) + hash_cache::poseidon_array_get(self.elements.as_slice()).unwrap_or_else(|| { + let result = Poseidon::hash_array(self.elements.as_slice()); + hash_cache::poseidon_array_insert(self.elements.as_slice(), result); + result + }) } } diff --git a/crates/starknet_api/src/hash.rs b/crates/starknet_api/src/hash.rs index ca0b3a72a33..71337952eab 100644 --- a/crates/starknet_api/src/hash.rs +++ b/crates/starknet_api/src/hash.rs @@ -5,10 +5,11 @@ use sha3::{Digest, Keccak256}; use starknet_types_core::felt::{Felt, FromStrError}; use starknet_types_core::hash::{Poseidon, StarkHash as StarkHashTrait}; -use crate::core::{ContractAddress, EntryPointSelector, GlobalRoot, Nonce, GLOBAL_STATE_VERSION}; +use crate::core::{ContractAddress, EntryPointSelector, GLOBAL_STATE_VERSION, GlobalRoot, Nonce}; +use crate::hash_cache; use crate::serde_utils::bytes_from_hex_str; -use crate::transaction::fields::Calldata; use crate::transaction::L1HandlerTransaction; +use crate::transaction::fields::Calldata; pub type StarkHash = Felt; @@ -30,11 +31,16 @@ pub struct PoseidonHash(pub Felt); /// Computes the first 250 bits of the Keccak256 hash, in order to fit into a field element. pub fn starknet_keccak_hash(input: &[u8]) -> Felt { + if let Some(cached) = hash_cache::sn_keccak_get(input) { + return cached; + } let mut keccak = Keccak256::default(); keccak.update(input); let mut hashed_bytes: [u8; 32] = keccak.finalize().into(); hashed_bytes[0] &= 0b00000011_u8; // Discard the six MSBs. - Felt::from_bytes_be(&hashed_bytes) + let result = Felt::from_bytes_be(&hashed_bytes); + hash_cache::sn_keccak_insert(input, result); + result } #[cfg(any(feature = "testing", test))] diff --git a/crates/starknet_api/src/hash_cache.rs b/crates/starknet_api/src/hash_cache.rs new file mode 100644 index 00000000000..a4afd77e70c --- /dev/null +++ b/crates/starknet_api/src/hash_cache.rs @@ -0,0 +1,342 @@ +use std::{ + borrow::Borrow, + hash::Hash, + sync::{ + LazyLock, + atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, + }, +}; + +use dashmap::DashMap; +use starknet_types_core::felt::Felt; + +pub const DEFAULT_SN_KECCAK_CACHE_CAPACITY: usize = 8 * 1024; +pub const DEFAULT_PEDERSEN_PAIR_CACHE_CAPACITY: usize = 8 * 1024; +pub const DEFAULT_PEDERSEN_ARRAY_CACHE_CAPACITY: usize = 2 * 1024; +pub const DEFAULT_POSEIDON_ARRAY_CACHE_CAPACITY: usize = 2 * 1024; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HashCacheConfig { + pub enabled: bool, + pub sn_keccak_capacity: usize, + pub pedersen_pair_capacity: usize, + pub pedersen_array_capacity: usize, + pub poseidon_array_capacity: usize, +} + +impl Default for HashCacheConfig { + fn default() -> Self { + Self { + enabled: false, + sn_keccak_capacity: DEFAULT_SN_KECCAK_CACHE_CAPACITY, + pedersen_pair_capacity: DEFAULT_PEDERSEN_PAIR_CACHE_CAPACITY, + pedersen_array_capacity: DEFAULT_PEDERSEN_ARRAY_CACHE_CAPACITY, + poseidon_array_capacity: DEFAULT_POSEIDON_ARRAY_CACHE_CAPACITY, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HashCacheKind { + SnKeccak, + PedersenPair, + PedersenArray, + PoseidonArray, +} + +impl HashCacheKind { + pub const fn as_str(self) -> &'static str { + match self { + Self::SnKeccak => "starknet_keccak", + Self::PedersenPair => "pedersen_pair", + Self::PedersenArray => "pedersen_array", + Self::PoseidonArray => "poseidon_array", + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct HashCacheMetrics { + pub kind: HashCacheKind, + pub total_calls: u64, + pub hits: u64, + pub misses: u64, + pub capacity_clears: u64, + pub entries: usize, + pub capacity: usize, +} + +struct CacheCounters { + hits: AtomicU64, + misses: AtomicU64, + capacity_clears: AtomicU64, +} + +impl CacheCounters { + const fn new() -> Self { + Self { + hits: AtomicU64::new(0), + misses: AtomicU64::new(0), + capacity_clears: AtomicU64::new(0), + } + } +} + +static HASH_CACHE_ENABLED: AtomicBool = AtomicBool::new(false); +static SN_KECCAK_CACHE_CAPACITY: AtomicUsize = AtomicUsize::new(DEFAULT_SN_KECCAK_CACHE_CAPACITY); +static PEDERSEN_PAIR_CACHE_CAPACITY: AtomicUsize = + AtomicUsize::new(DEFAULT_PEDERSEN_PAIR_CACHE_CAPACITY); +static PEDERSEN_ARRAY_CACHE_CAPACITY: AtomicUsize = + AtomicUsize::new(DEFAULT_PEDERSEN_ARRAY_CACHE_CAPACITY); +static POSEIDON_ARRAY_CACHE_CAPACITY: AtomicUsize = + AtomicUsize::new(DEFAULT_POSEIDON_ARRAY_CACHE_CAPACITY); +static SN_KECCAK_CACHE: LazyLock, Felt>> = LazyLock::new(DashMap::new); +static PEDERSEN_PAIR_CACHE: LazyLock> = LazyLock::new(DashMap::new); +static PEDERSEN_ARRAY_CACHE: LazyLock, Felt>> = LazyLock::new(DashMap::new); +static POSEIDON_ARRAY_CACHE: LazyLock, Felt>> = LazyLock::new(DashMap::new); +static SN_KECCAK_CACHE_COUNTERS: CacheCounters = CacheCounters::new(); +static PEDERSEN_PAIR_CACHE_COUNTERS: CacheCounters = CacheCounters::new(); +static PEDERSEN_ARRAY_CACHE_COUNTERS: CacheCounters = CacheCounters::new(); +static POSEIDON_ARRAY_CACHE_COUNTERS: CacheCounters = CacheCounters::new(); + +/// Configures process-wide Starknet hash memoization. +/// +/// Configure this once during process startup, before execution workers begin +/// handling transactions. A capacity of zero disables that individual cache. +pub fn configure_hash_cache(config: HashCacheConfig) { + HASH_CACHE_ENABLED.store(false, Ordering::Relaxed); + clear_caches(); + SN_KECCAK_CACHE_CAPACITY.store(config.sn_keccak_capacity, Ordering::Relaxed); + PEDERSEN_PAIR_CACHE_CAPACITY.store(config.pedersen_pair_capacity, Ordering::Relaxed); + PEDERSEN_ARRAY_CACHE_CAPACITY.store(config.pedersen_array_capacity, Ordering::Relaxed); + POSEIDON_ARRAY_CACHE_CAPACITY.store(config.poseidon_array_capacity, Ordering::Relaxed); + HASH_CACHE_ENABLED.store(config.enabled, Ordering::Relaxed); +} + +/// Enables or disables process-wide Starknet hash memoization. +/// +/// Configure this once during process startup, before execution workers begin +/// handling transactions. +pub fn set_hash_cache_enabled(enabled: bool) { + HASH_CACHE_ENABLED.store(enabled, Ordering::Relaxed); + if !enabled { + clear_caches(); + } +} + +pub fn hash_cache_metrics() -> [HashCacheMetrics; 4] { + [ + snapshot( + HashCacheKind::SnKeccak, + &SN_KECCAK_CACHE, + &SN_KECCAK_CACHE_CAPACITY, + &SN_KECCAK_CACHE_COUNTERS, + ), + snapshot( + HashCacheKind::PedersenPair, + &PEDERSEN_PAIR_CACHE, + &PEDERSEN_PAIR_CACHE_CAPACITY, + &PEDERSEN_PAIR_CACHE_COUNTERS, + ), + snapshot( + HashCacheKind::PedersenArray, + &PEDERSEN_ARRAY_CACHE, + &PEDERSEN_ARRAY_CACHE_CAPACITY, + &PEDERSEN_ARRAY_CACHE_COUNTERS, + ), + snapshot( + HashCacheKind::PoseidonArray, + &POSEIDON_ARRAY_CACHE, + &POSEIDON_ARRAY_CACHE_CAPACITY, + &POSEIDON_ARRAY_CACHE_COUNTERS, + ), + ] +} + +fn snapshot( + kind: HashCacheKind, + cache: &DashMap, + capacity: &AtomicUsize, + counters: &CacheCounters, +) -> HashCacheMetrics +where + K: Eq + Hash, +{ + let hits = counters.hits.load(Ordering::Relaxed); + let misses = counters.misses.load(Ordering::Relaxed); + HashCacheMetrics { + kind, + total_calls: hits.saturating_add(misses), + hits, + misses, + capacity_clears: counters.capacity_clears.load(Ordering::Relaxed), + entries: cache.len(), + capacity: capacity.load(Ordering::Relaxed), + } +} + +fn clear_caches() { + SN_KECCAK_CACHE.clear(); + PEDERSEN_PAIR_CACHE.clear(); + PEDERSEN_ARRAY_CACHE.clear(); + POSEIDON_ARRAY_CACHE.clear(); +} + +fn enabled() -> bool { + HASH_CACHE_ENABLED.load(Ordering::Relaxed) +} + +fn get(cache: &DashMap, key: &Q, counters: &CacheCounters) -> Option +where + K: Borrow + Eq + Hash, + Q: Eq + Hash + ?Sized, + V: Copy, +{ + if !enabled() { + return None; + } + match cache.get(key) { + Some(value) => { + counters.hits.fetch_add(1, Ordering::Relaxed); + Some(*value) + } + None => { + counters.misses.fetch_add(1, Ordering::Relaxed); + None + } + } +} + +fn insert( + cache: &DashMap, + capacity: &AtomicUsize, + counters: &CacheCounters, + key: K, + value: V, +) where + K: Eq + Hash, +{ + if !enabled() { + return; + } + let capacity = capacity.load(Ordering::Relaxed); + if capacity == 0 { + return; + } + if cache.len() >= capacity { + cache.clear(); + counters.capacity_clears.fetch_add(1, Ordering::Relaxed); + } + cache.insert(key, value); +} + +pub(crate) fn sn_keccak_get(data: &[u8]) -> Option { + get(&SN_KECCAK_CACHE, data, &SN_KECCAK_CACHE_COUNTERS) +} + +pub(crate) fn sn_keccak_insert(data: &[u8], value: Felt) { + insert( + &SN_KECCAK_CACHE, + &SN_KECCAK_CACHE_CAPACITY, + &SN_KECCAK_CACHE_COUNTERS, + data.to_vec(), + value, + ); +} + +pub(crate) fn pedersen_pair_get(left: Felt, right: Felt) -> Option { + get(&PEDERSEN_PAIR_CACHE, &(left, right), &PEDERSEN_PAIR_CACHE_COUNTERS) +} + +pub(crate) fn pedersen_pair_insert(left: Felt, right: Felt, value: Felt) { + insert( + &PEDERSEN_PAIR_CACHE, + &PEDERSEN_PAIR_CACHE_CAPACITY, + &PEDERSEN_PAIR_CACHE_COUNTERS, + (left, right), + value, + ); +} + +pub(crate) fn pedersen_array_get(values: &[Felt]) -> Option { + get(&PEDERSEN_ARRAY_CACHE, values, &PEDERSEN_ARRAY_CACHE_COUNTERS) +} + +pub(crate) fn pedersen_array_insert(values: &[Felt], value: Felt) { + insert( + &PEDERSEN_ARRAY_CACHE, + &PEDERSEN_ARRAY_CACHE_CAPACITY, + &PEDERSEN_ARRAY_CACHE_COUNTERS, + values.to_vec(), + value, + ); +} + +pub(crate) fn poseidon_array_get(values: &[Felt]) -> Option { + get(&POSEIDON_ARRAY_CACHE, values, &POSEIDON_ARRAY_CACHE_COUNTERS) +} + +pub(crate) fn poseidon_array_insert(values: &[Felt], value: Felt) { + insert( + &POSEIDON_ARRAY_CACHE, + &POSEIDON_ARRAY_CACHE_CAPACITY, + &POSEIDON_ARRAY_CACHE_COUNTERS, + values.to_vec(), + value, + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + static TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + #[test] + fn cache_is_bypassed_when_disabled_and_reused_when_enabled() { + let _guard = TEST_MUTEX.lock().unwrap(); + let key = Felt::from(1_u8); + let value = Felt::from(2_u8); + + configure_hash_cache(HashCacheConfig::default()); + pedersen_pair_insert(key, key, value); + assert_eq!(pedersen_pair_get(key, key), None); + + set_hash_cache_enabled(true); + pedersen_pair_insert(key, key, value); + assert_eq!(pedersen_pair_get(key, key), Some(value)); + + set_hash_cache_enabled(false); + } + + #[test] + fn capacity_and_metrics_are_configurable() { + let _guard = TEST_MUTEX.lock().unwrap(); + let before = hash_cache_metrics()[1]; + configure_hash_cache(HashCacheConfig { + enabled: true, + pedersen_pair_capacity: 1, + ..Default::default() + }); + + let first = Felt::from(1_u8); + let second = Felt::from(2_u8); + let value = Felt::from(3_u8); + assert_eq!(pedersen_pair_get(first, first), None); + pedersen_pair_insert(first, first, value); + assert_eq!(pedersen_pair_get(first, first), Some(value)); + pedersen_pair_insert(second, second, value); + + let after = hash_cache_metrics()[1]; + assert_eq!(after.capacity, 1); + assert_eq!(after.entries, 1); + assert_eq!(after.total_calls, before.total_calls + 2); + assert_eq!(after.hits, before.hits + 1); + assert_eq!(after.misses, before.misses + 1); + assert_eq!(after.capacity_clears, before.capacity_clears + 1); + assert_eq!(pedersen_pair_get(first, first), None); + assert_eq!(pedersen_pair_get(second, second), Some(value)); + + configure_hash_cache(HashCacheConfig::default()); + } +} diff --git a/crates/starknet_api/src/lib.rs b/crates/starknet_api/src/lib.rs index 357fe74ce90..487fb9674c3 100644 --- a/crates/starknet_api/src/lib.rs +++ b/crates/starknet_api/src/lib.rs @@ -17,6 +17,13 @@ pub mod executable_transaction; pub mod execution_resources; pub mod execution_utils; pub mod hash; +mod hash_cache; +pub use hash_cache::{ + DEFAULT_PEDERSEN_ARRAY_CACHE_CAPACITY, DEFAULT_PEDERSEN_PAIR_CACHE_CAPACITY, + DEFAULT_POSEIDON_ARRAY_CACHE_CAPACITY, DEFAULT_SN_KECCAK_CACHE_CAPACITY, HashCacheConfig, + HashCacheKind, HashCacheMetrics, configure_hash_cache, hash_cache_metrics, + set_hash_cache_enabled, +}; pub mod rpc_transaction; pub mod serde_utils; pub mod staking;