diff --git a/Cargo.lock b/Cargo.lock index d872209e..0a4bf904 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -643,6 +643,7 @@ dependencies = [ "katana-primitives", "katana-rpc", "katana-rpc-api", + "lru", "once_cell", "regex", "reqwest 0.12.8", @@ -3697,6 +3698,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -4906,6 +4913,17 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashers" version = "1.0.1" @@ -6335,11 +6353,11 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d55165dd..2ae1e9ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ bitvec = "1.0.1" validator = { version = "0.18.1", features = ["derive"] } url = "2.5.1" toml = "0.8.19" +lru = "0.12.5" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/src/exe/cache.rs b/src/exe/cache.rs new file mode 100644 index 00000000..a9ff4555 --- /dev/null +++ b/src/exe/cache.rs @@ -0,0 +1,254 @@ +use blockifier::state::state_api::{State as BlockifierState, StateReader}; +use ethers::types::U256; +use lru::LruCache; +use starknet_api::{core::ContractAddress, state::StorageKey}; +use starknet_types_core::felt::Felt as StarkFelt; +use std::num::NonZeroUsize; +use std::sync::{LazyLock, Mutex}; + +use crate::gen; + +mod storage { + use super::*; + + type Key = (U256, U256, U256); // block hash + contract address + storage key + type Value = StarkFelt; + + const SIZE: usize = 1024; + + static CACHE: LazyLock>> = LazyLock::new(|| { + Mutex::new(LruCache::new(NonZeroUsize::new(SIZE).unwrap())) + }); + + pub fn get(key: &Key) -> Option { + let mut guard = CACHE.lock().expect("storage-cache-lock"); + guard.get(key).cloned() + } + + pub fn set(key: Key, value: Value) -> Option { + let mut guard = CACHE.lock().expect("storage-cache-lock"); + guard.put(key, value) + } + + pub fn key( + block_hash: &gen::Felt, + contract_address: &ContractAddress, + storage_key: &StorageKey, + ) -> Key { + ( + block_hash.as_ref().parse().unwrap(), + contract_address.0.key().to_bytes_be().into(), + storage_key.0.key().to_bytes_be().into(), + ) + } +} + +mod class_hash { + use super::*; + + type Key = (U256, U256); // block hash + contract address + type Value = starknet_api::core::ClassHash; + + const SIZE: usize = 256; + + static CACHE: LazyLock>> = LazyLock::new(|| { + Mutex::new(LruCache::new(NonZeroUsize::new(SIZE).unwrap())) + }); + + pub fn get(key: &Key) -> Option { + let mut guard = CACHE.lock().expect("classhash-cache-lock"); + guard.get(key).cloned() + } + + pub fn set(key: Key, value: Value) -> Option { + let mut guard = CACHE.lock().expect("classhash-cache-lock"); + guard.put(key, value) + } + + pub fn key( + block_hash: &gen::Felt, + contract_address: &ContractAddress, + ) -> Key { + ( + block_hash.as_ref().parse().unwrap(), + contract_address.0.key().to_bytes_be().into(), + ) + } +} + +mod contract_class { + use super::*; + + type Key = (U256, U256); // block hash + class hash + type Value = blockifier::execution::contract_class::ContractClass; + + const SIZE: usize = 256; + + static CACHE: LazyLock>> = LazyLock::new(|| { + Mutex::new(LruCache::new(NonZeroUsize::new(SIZE).unwrap())) + }); + + pub fn get(key: &Key) -> Option { + let mut guard = CACHE.lock().expect("contractclass-cache-lock"); + guard.get(key).cloned() + } + + pub fn set(key: Key, value: Value) -> Option { + let mut guard = CACHE.lock().expect("contractclass-cache-lock"); + guard.put(key, value) + } + + pub fn key( + block_hash: &gen::Felt, + class_hash: &starknet_api::core::ClassHash, + ) -> Key { + ( + block_hash.as_ref().parse().unwrap(), + class_hash.0.to_bytes_be().into(), + ) + } +} + +pub trait HasBlockHash { + fn get_block_hash(&self) -> &gen::Felt; +} + +pub struct CachedState { + inner: T, +} + +impl CachedState { + pub fn new(inner: T) -> Self { + Self { inner } + } +} + +impl StateReader + for CachedState +{ + fn get_storage_at( + &self, + contract_address: ContractAddress, + storage_key: StorageKey, + ) -> blockifier::state::state_api::StateResult { + let block_hash = self.inner.get_block_hash(); + if let Some(ret) = storage::get(&storage::key( + block_hash, + &contract_address, + &storage_key, + )) { + return Ok(ret); + } + let ret = self.inner.get_storage_at(contract_address, storage_key)?; + storage::set( + storage::key(block_hash, &contract_address, &storage_key), + ret, + ); + Ok(ret) + } + + fn get_nonce_at( + &self, + contract_address: ContractAddress, + ) -> blockifier::state::state_api::StateResult + { + self.inner.get_nonce_at(contract_address) + } + + fn get_class_hash_at( + &self, + contract_address: ContractAddress, + ) -> blockifier::state::state_api::StateResult + { + let block_hash = self.inner.get_block_hash(); + if let Some(ret) = + class_hash::get(&class_hash::key(block_hash, &contract_address)) + { + return Ok(ret); + } + let ret = self.inner.get_class_hash_at(contract_address)?; + class_hash::set(class_hash::key(block_hash, &contract_address), ret); + Ok(ret) + } + + fn get_compiled_contract_class( + &self, + class_hash: starknet_api::core::ClassHash, + ) -> blockifier::state::state_api::StateResult< + blockifier::execution::contract_class::ContractClass, + > { + let block_hash = self.inner.get_block_hash(); + if let Some(ret) = + contract_class::get(&contract_class::key(block_hash, &class_hash)) + { + return Ok(ret); + } + let ret = self.inner.get_compiled_contract_class(class_hash)?; + contract_class::set( + contract_class::key(block_hash, &class_hash), + ret.clone(), + ); + Ok(ret) + } + + fn get_compiled_class_hash( + &self, + class_hash: starknet_api::core::ClassHash, + ) -> blockifier::state::state_api::StateResult< + starknet_api::core::CompiledClassHash, + > { + self.inner.get_compiled_class_hash(class_hash) + } +} + +impl BlockifierState + for CachedState +{ + fn set_storage_at( + &mut self, + contract_address: ContractAddress, + storage_key: StorageKey, + value: StarkFelt, + ) -> blockifier::state::state_api::StateResult<()> { + self.inner.set_storage_at(contract_address, storage_key, value) + } + + fn increment_nonce( + &mut self, + contract_address: ContractAddress, + ) -> blockifier::state::state_api::StateResult<()> { + self.inner.increment_nonce(contract_address) + } + + fn set_class_hash_at( + &mut self, + contract_address: ContractAddress, + class_hash: starknet_api::core::ClassHash, + ) -> blockifier::state::state_api::StateResult<()> { + self.inner.set_class_hash_at(contract_address, class_hash) + } + + fn set_contract_class( + &mut self, + class_hash: starknet_api::core::ClassHash, + contract_class: blockifier::execution::contract_class::ContractClass, + ) -> blockifier::state::state_api::StateResult<()> { + self.inner.set_contract_class(class_hash, contract_class) + } + + fn set_compiled_class_hash( + &mut self, + class_hash: starknet_api::core::ClassHash, + compiled_class_hash: starknet_api::core::CompiledClassHash, + ) -> blockifier::state::state_api::StateResult<()> { + self.inner.set_compiled_class_hash(class_hash, compiled_class_hash) + } + + fn add_visited_pcs( + &mut self, + class_hash: starknet_api::core::ClassHash, + pcs: &std::collections::HashSet, + ) { + self.inner.add_visited_pcs(class_hash, pcs); + } +} diff --git a/src/exe/mod.rs b/src/exe/mod.rs index 8c682191..68d0fb1b 100644 --- a/src/exe/mod.rs +++ b/src/exe/mod.rs @@ -40,6 +40,7 @@ use crate::{ gen::{self, blocking::Rpc}, }; +pub mod cache; pub mod err; pub mod map; @@ -110,11 +111,11 @@ pub fn call( }); let tx_context = Arc::new(TransactionContext { block_context, tx_info }); - + let limit_steps_by_resources = false; let mut context = EntryPointExecutionContext::new( tx_context.clone(), ExecutionMode::Execute, - /*limit_steps_by_resources=*/ false, + limit_steps_by_resources, )?; let call_entry_point = CallEntryPoint { @@ -129,11 +130,13 @@ pub fn call( initial_gas: u64::MAX, }; - let mut proxy: StateProxy = StateProxy { client, state }; + let state_proxy: StateProxy = StateProxy { client, state }; + let mut state_proxy = cache::CachedState::new(state_proxy); + let mut resources = Default::default(); let call_info = call_entry_point.execute( - &mut proxy, - &mut Default::default(), + &mut state_proxy, + &mut resources, &mut context, )?; @@ -146,18 +149,26 @@ struct StateProxy { state: State, } +impl cache::HasBlockHash + for StateProxy +{ + fn get_block_hash(&self) -> &gen::Felt { + &self.state.block_hash + } +} + impl StateReader for StateProxy { fn get_storage_at( &self, contract_address: ContractAddress, - key: StarknetStorageKey, + storage_key: StarknetStorageKey, ) -> StateResult { - tracing::info!(?contract_address, ?key, "get_storage_at"); + tracing::info!(?contract_address, ?storage_key, "get_storage_at"); let felt: gen::Felt = contract_address.0.key().try_into()?; - let contract_address = gen::Address(felt); + let address = gen::Address(felt); - let key = gen::StorageKey::try_new(&key.0.to_string()) + let key = gen::StorageKey::try_new(&storage_key.0.to_string()) .map_err(Into::::into)?; let block_id = gen::BlockId::BlockHash { @@ -166,13 +177,9 @@ impl StateReader for StateProxy { let ret = self .client - .getStorageAt( - contract_address.clone(), - key.clone(), - block_id.clone(), - ) + .getStorageAt(address.clone(), key.clone(), block_id.clone()) .map_err(Into::::into)?; - tracing::info!(?contract_address, ?key, value=?ret, "get_storage_at"); + tracing::info!(?address, ?key, value=?ret, "get_storage_at"); if ret.as_ref() == "0x0" { tracing::info!("get_storage_at: skipping proof for zero value"); @@ -181,19 +188,17 @@ impl StateReader for StateProxy { let proof = self .client - .getProof(block_id, contract_address.clone(), vec![key.clone()]) + .getProof(block_id, address.clone(), vec![key.clone()]) .map_err(Into::::into)?; tracing::info!("get_storage_at: proof received"); let global_root = self.state.root.clone(); let value = ret.clone(); - proof.verify(global_root, contract_address, key, value).map_err( - |e| { - StateError::StateReadError(format!( - "Failed to verify merkle proof: {e:?}" - )) - }, - )?; + proof.verify(global_root, address, key, value).map_err(|e| { + StateError::StateReadError(format!( + "Failed to verify merkle proof: {e:?}" + )) + })?; tracing::info!("get_storage_at: proof verified"); Ok(ret.try_into()?) @@ -205,7 +210,9 @@ impl StateReader for StateProxy { ) -> StateResult { tracing::info!(?contract_address, "get_nonce_at"); - let block_id = gen::BlockId::BlockTag(gen::BlockTag::Latest); + let block_id = gen::BlockId::BlockHash { + block_hash: gen::BlockHash(self.state.block_hash.clone()), + }; let felt: gen::Felt = contract_address.0.key().try_into()?; let contract_address = gen::Address(felt); @@ -224,7 +231,9 @@ impl StateReader for StateProxy { ) -> StateResult { tracing::info!(?contract_address, "get_class_hash_at"); - let block_id = gen::BlockId::BlockTag(gen::BlockTag::Latest); + let block_id = gen::BlockId::BlockHash { + block_hash: gen::BlockHash(self.state.block_hash.clone()), + }; let felt: gen::Felt = contract_address.0.key().try_into()?; let contract_address = gen::Address(felt); @@ -243,7 +252,9 @@ impl StateReader for StateProxy { ) -> StateResult { tracing::info!(?class_hash, "get_compiled_contract_class"); - let block_id = gen::BlockId::BlockTag(gen::BlockTag::Latest); + let block_id = gen::BlockId::BlockHash { + block_hash: gen::BlockHash(self.state.block_hash.clone()), + }; let class_hash: gen::Felt = class_hash.0.try_into()?; diff --git a/web/beerus-web/Cargo.lock b/web/beerus-web/Cargo.lock index 77575f97..7ebd8e65 100644 --- a/web/beerus-web/Cargo.lock +++ b/web/beerus-web/Cargo.lock @@ -414,6 +414,7 @@ dependencies = [ "helios", "hex", "iamgroot", + "lru", "once_cell", "regex", "reqwest 0.12.9", diff --git a/web/beerus-web/src/lib.rs b/web/beerus-web/src/lib.rs index 20ad1f6c..9d39c360 100644 --- a/web/beerus-web/src/lib.rs +++ b/web/beerus-web/src/lib.rs @@ -152,6 +152,7 @@ pub fn set_panic_hook() { #[wasm_bindgen] pub struct Beerus { beerus: beerus::client::Client, + state: Option, } #[wasm_bindgen] @@ -167,37 +168,42 @@ impl Beerus { ethereum_rpc: config.ethereum_url, starknet_rpc: config.starknet_url, }; - web_sys::console::log_1(&"beerus: config ready".into()); let beerus = beerus::client::Client::new(&config, Http(Rc::new(f))) .await .map_err(|e| JsValue::from_str(&format!("failed to create client: {e:?}")))?; - web_sys::console::log_1(&"beerus: client ready".into()); - Ok(Self { beerus }) + web_sys::console::log_1(&"beerus: ready".into()); + Ok(Self { beerus, state: None }) } #[wasm_bindgen] - pub async fn get_state(&self) -> Result { + pub async fn get_state(&mut self) -> Result { let state = self.beerus.get_state().await .map_err(|e| JsValue::from_str(&format!("failed to get state: {e:?}")))?; - let state = serde_json::to_string(&dto::State { + + let ret = serde_json::to_string(&dto::State { len: state.block_number as i64, hash: state.block_hash.as_ref().to_owned(), root: state.root.as_ref().to_owned(), }).map_err(|e| JsValue::from_str(&format!("failed to serialize state: {e:?}")))?; - Ok(JsValue::from_str(&state)) + let ret = JsValue::from_str(&ret); + + self.state = Some(state); + Ok(ret) } #[wasm_bindgen] - pub async fn execute(&self, request: &str) -> Result { - let state = self.beerus.get_state().await - .map_err(|e| JsValue::from_str(&format!("failed to get state: {e:?}")))?; - web_sys::console::log_1(&"beerus: execute: state ready".into()); + pub async fn execute(&mut self, request: &str) -> Result { + if self.state.is_none() { + let _ = self.get_state().await?; + } + let state = self.state.clone().unwrap(); + let request: beerus::gen::FunctionCall = serde_json::from_str(request) .map_err(|e| JsValue::from_str(&format!("failed to parse request: {e:?}")))?; - web_sys::console::log_1(&"beerus: execute: request ready".into()); + let result = self.beerus.execute(request, state) .map_err(|e| JsValue::from_str(&format!("failed to execute call: {e:?}")))?; - web_sys::console::log_1(&format!("beerus: execute: {result:?}").into()); + let result = serde_json::to_string(&result) .map_err(|e| JsValue::from_str(&format!("failed to serialize call result: {e:?}")))?; Ok(JsValue::from_str(&result))