diff --git a/bin/debug-trace-server/src/chain_sync.rs b/bin/debug-trace-server/src/chain_sync.rs index 4e7ff299..a1caf2ff 100644 --- a/bin/debug-trace-server/src/chain_sync.rs +++ b/bin/debug-trace-server/src/chain_sync.rs @@ -201,13 +201,10 @@ mod tests { // Minimal mock for test compilation struct MockBlockStore; impl stateless_core::ContractStore for MockBlockStore { - fn get_contracts( - &self, - _: &[B256], - ) -> StoreResult<(HashMap>, Vec)> { + fn get_contracts(&self, _: &[B256]) -> StoreResult<(HashMap, Vec)> { Ok((Default::default(), vec![])) } - fn add_contracts(&self, _: &[(B256, Arc)]) -> StoreResult<()> { + fn add_contracts(&self, _: &[(B256, Bytecode)]) -> StoreResult<()> { Ok(()) } } diff --git a/bin/debug-trace-server/src/data_provider.rs b/bin/debug-trace-server/src/data_provider.rs index 27e3c6ba..b5d3bf27 100644 --- a/bin/debug-trace-server/src/data_provider.rs +++ b/bin/debug-trace-server/src/data_provider.rs @@ -65,8 +65,9 @@ pub struct BlockData { /// Light witness without expensive EC point validation. pub witness: LightWitness, /// Contract bytecodes keyed by code hash, required for EVM execution. - /// Values share allocations with the `ContractCache`. - pub contracts: HashMap>, + /// `Bytecode` is internally reference-counted, so values share their underlying allocation + /// with the `ContractCache` (and across `BlockData` clones) via cheap refcount-bump clones. + pub contracts: HashMap, } /// Default timeout for a user-facing witness fetch in seconds (8 seconds). @@ -554,7 +555,7 @@ impl DataProvider { &self, code_hashes: &[B256], deadline: Instant, - ) -> DataProviderResult>> { + ) -> DataProviderResult> { resolve_contracts_inner(&self.rpc_client, &self.contract_cache, code_hashes, deadline).await } } @@ -753,7 +754,7 @@ async fn resolve_contracts_inner( contract_cache: &ContractCache, code_hashes: &[B256], deadline: Instant, -) -> DataProviderResult>> { +) -> DataProviderResult> { let (mut contracts, missing) = contract_cache.get(code_hashes)?; if missing.is_empty() { @@ -771,7 +772,7 @@ async fn resolve_contracts_inner( // `TraceRpcMetrics` adapter inside `round_robin_with_backoff`. let fetched = rpc_client.get_codes_with_deadline(&missing, true, Some(deadline)).await?; - let new_contracts: Vec<(B256, Arc)> = fetched.into_iter().collect(); + let new_contracts: Vec<(B256, Bytecode)> = fetched.into_iter().collect(); // Write-through: memory always, disk in local-cache mode. We don't fail the trace on // cache-insert errors; the request has already been served. @@ -791,14 +792,11 @@ async fn resolve_contracts_inner( pub(crate) struct NoopContractStore; impl ContractStore for NoopContractStore { - fn get_contracts( - &self, - hashes: &[B256], - ) -> StoreResult<(HashMap>, Vec)> { + fn get_contracts(&self, hashes: &[B256]) -> StoreResult<(HashMap, Vec)> { Ok((HashMap::default(), hashes.to_vec())) } - fn add_contracts(&self, _codes: &[(B256, Arc)]) -> StoreResult<()> { + fn add_contracts(&self, _codes: &[(B256, Bytecode)]) -> StoreResult<()> { Ok(()) } } diff --git a/bin/debug-trace-server/src/server_db.rs b/bin/debug-trace-server/src/server_db.rs index 720a40d9..479ec476 100644 --- a/bin/debug-trace-server/src/server_db.rs +++ b/bin/debug-trace-server/src/server_db.rs @@ -3,7 +3,7 @@ //! Provides persistent storage of block data, witnesses, and canonical chain state //! for serving `debug_*` and `trace_*` RPC methods. -use std::{path::Path, sync::Arc}; +use std::path::Path; use alloy_primitives::{B256, BlockHash, BlockNumber, map::HashMap}; use alloy_rpc_types_eth::Block; @@ -218,14 +218,11 @@ impl ServerDB { } impl ContractStore for ServerDB { - fn get_contracts( - &self, - hashes: &[B256], - ) -> StoreResult<(HashMap>, Vec)> { + fn get_contracts(&self, hashes: &[B256]) -> StoreResult<(HashMap, Vec)> { read_contracts(&self.database, hashes) } - fn add_contracts(&self, codes: &[(B256, Arc)]) -> StoreResult<()> { + fn add_contracts(&self, codes: &[(B256, Bytecode)]) -> StoreResult<()> { write_add_contracts(&self.database, codes) } } @@ -352,8 +349,7 @@ mod tests { let hash1 = B256::from([1u8; 32]); let hash2 = B256::from([2u8; 32]); - let bytecode = - Arc::new(Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00]))); + let bytecode = Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00])); ContractStore::add_contracts(&db, &[(hash1, bytecode.clone())]).unwrap(); diff --git a/bin/debug-trace-server/src/tracing_executor.rs b/bin/debug-trace-server/src/tracing_executor.rs index d1c6c8f4..9fb8ba6f 100644 --- a/bin/debug-trace-server/src/tracing_executor.rs +++ b/bin/debug-trace-server/src/tracing_executor.rs @@ -24,8 +24,6 @@ //! ## Parity-style (trace_* methods) //! - `LocalizedTransactionTrace` - Flat call traces with block/tx context -use std::sync::Arc; - use alloy_consensus::Transaction; use alloy_evm::{Evm as EvmTrait, block::BlockExecutor}; use alloy_op_evm::block::OpAlloyReceiptBuilder; @@ -167,7 +165,7 @@ impl<'a> TracingEnv<'a> { fn create_witness_db<'b>( &'b self, - contracts: &'b HashMap>, + contracts: &'b HashMap, ) -> WitnessDatabase<'b, LightWitnessExecutor> { WitnessDatabase { header: self.header, witness: &self.light_witness_executor, contracts } } @@ -444,7 +442,7 @@ pub fn trace_block( chain_spec: &ChainSpec, block: &Block, witness: LightWitness, - contracts: &HashMap>, + contracts: &HashMap, opts: GethDebugTracingOptions, ) -> Result, ValidationError> { let env = TracingEnv::new(chain_spec, block, witness)?; @@ -710,7 +708,7 @@ pub fn trace_transaction( block: &Block, tx_index: usize, light_witness: LightWitness, - contracts: &HashMap>, + contracts: &HashMap, opts: GethDebugTracingOptions, ) -> Result { let env = TracingEnv::new(chain_spec, block, light_witness)?; @@ -877,7 +875,7 @@ pub fn parity_trace_block( chain_spec: &ChainSpec, block: &Block, light_witness: LightWitness, - contracts: &HashMap>, + contracts: &HashMap, ) -> Result, ValidationError> { let env = TracingEnv::new(chain_spec, block, light_witness)?; @@ -927,7 +925,7 @@ pub fn parity_trace_transaction( block: &Block, tx_index: usize, light_witness: LightWitness, - contracts: &HashMap>, + contracts: &HashMap, ) -> Result, ValidationError> { let env = TracingEnv::new(chain_spec, block, light_witness)?; diff --git a/bin/stateless-validator/src/chain_sync.rs b/bin/stateless-validator/src/chain_sync.rs index 10f5fcee..9f758801 100644 --- a/bin/stateless-validator/src/chain_sync.rs +++ b/bin/stateless-validator/src/chain_sync.rs @@ -196,7 +196,7 @@ impl BlockProcessor for ValidatorProcessor { } })?; - let new_bytecodes: Vec<(B256, Arc)> = fetched.into_iter().collect(); + let new_bytecodes: Vec<(B256, Bytecode)> = fetched.into_iter().collect(); self.contract_cache .insert(&new_bytecodes) diff --git a/bin/stateless-validator/src/validator_db.rs b/bin/stateless-validator/src/validator_db.rs index 7cce3264..0545b68b 100644 --- a/bin/stateless-validator/src/validator_db.rs +++ b/bin/stateless-validator/src/validator_db.rs @@ -6,7 +6,7 @@ //! CANONICAL_CHAIN is bounded to `max_chain_length` entries; older entries are //! pruned inline during [`ChainStore::advance_chain`]. -use std::{path::Path, sync::Arc}; +use std::path::Path; use alloy_genesis::Genesis; use alloy_primitives::{B256, BlockHash, BlockNumber, map::HashMap}; @@ -73,14 +73,11 @@ impl ValidatorDB { } impl ContractStore for ValidatorDB { - fn get_contracts( - &self, - hashes: &[B256], - ) -> StoreResult<(HashMap>, Vec)> { + fn get_contracts(&self, hashes: &[B256]) -> StoreResult<(HashMap, Vec)> { read_contracts(&self.database, hashes) } - fn add_contracts(&self, codes: &[(B256, Arc)]) -> StoreResult<()> { + fn add_contracts(&self, codes: &[(B256, Bytecode)]) -> StoreResult<()> { write_add_contracts(&self.database, codes) } } @@ -227,10 +224,8 @@ mod tests { let hash2 = B256::from([2u8; 32]); let hash3 = B256::from([3u8; 32]); - let bytecode1 = - Arc::new(Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00]))); - let bytecode2 = - Arc::new(Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x01]))); + let bytecode1 = Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00])); + let bytecode2 = Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x01])); store.add_contracts(&[(hash1, bytecode1.clone()), (hash2, bytecode2.clone())]).unwrap(); @@ -282,8 +277,7 @@ mod tests { let cache = ContractCache::new(Arc::new(store)); let hash = B256::from([1u8; 32]); - let bytecode = - Arc::new(Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00]))); + let bytecode = Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00])); cache.insert(&[(hash, bytecode.clone())]).unwrap(); @@ -299,8 +293,7 @@ mod tests { let db_path = dir.path().join("test.redb"); let hash = B256::from([1u8; 32]); - let bytecode = - Arc::new(Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00]))); + let bytecode = Bytecode::new_raw(alloy_primitives::Bytes::from_static(&[0x60, 0x00])); { let store = ValidatorDB::new(&db_path).unwrap(); diff --git a/crates/stateless-common/src/rpc_client.rs b/crates/stateless-common/src/rpc_client.rs index 73414611..07c4575c 100644 --- a/crates/stateless-common/src/rpc_client.rs +++ b/crates/stateless-common/src/rpc_client.rs @@ -654,15 +654,15 @@ impl RpcClient { /// /// # Return type /// - /// Returns `Arc` so the value shares one allocation with the - /// [`crate::ContractCache`] end-to-end. Callers that feed into the verified - /// `ContractCache` tiers should pass `verify = true`; the cache itself trusts memory/disk - /// hits and does not re-verify. + /// Returns plain `Bytecode`: it is already internally reference-counted, so it shares one + /// underlying allocation with the [`crate::ContractCache`] end-to-end without an outer `Arc`. + /// Callers that feed into the verified `ContractCache` tiers should pass `verify = true`; the + /// cache itself trusts memory/disk hits and does not re-verify. pub async fn get_codes( &self, hashes: &[B256], verify: bool, - ) -> std::result::Result>, CodeFetchError> { + ) -> std::result::Result, CodeFetchError> { self.get_codes_with_deadline(hashes, verify, None).await } @@ -677,7 +677,7 @@ impl RpcClient { hashes: &[B256], verify: bool, deadline: Option, - ) -> std::result::Result>, CodeFetchError> { + ) -> std::result::Result, CodeFetchError> { // `try_join_all` cancels the remaining per-hash futures on the first error — a // `VerificationFailure` or `Deadline` on one hash stops the rest of the batch // immediately and releases their concurrency permits, so a slow straggler can't @@ -691,7 +691,7 @@ impl RpcClient { return Err(CodeFetchError::VerificationFailure { requested: hash, actual }); } } - Ok::<_, CodeFetchError>((hash, Arc::new(code))) + Ok::<_, CodeFetchError>((hash, code)) })) .await .map(|pairs| pairs.into_iter().collect()) @@ -1655,10 +1655,10 @@ mod tests { handle.stop().unwrap(); } - /// All-good batch: verified entries are returned wrapped in `Arc` (the type - /// change that lets `ContractCache::insert` avoid a second allocation at each call site). + /// All-good batch: verified entries are returned as plain `Bytecode` (already internally + /// reference-counted, so `ContractCache::insert` shares the allocation without a second wrap). #[tokio::test] - async fn test_get_codes_verify_ok_returns_arc_bytecode() { + async fn test_get_codes_verify_ok_returns_bytecode() { let good_code = Bytes::from_static(&[0x60, 0x00, 0x60, 0x01]); let good_hash = Bytecode::new_raw(good_code.clone()).hash_slow(); @@ -1667,10 +1667,10 @@ mod tests { let ok = client.get_codes(&[good_hash], true).await.unwrap(); assert_eq!(ok.len(), 1); - let arc = ok.get(&good_hash).expect("good hash present"); - assert_eq!(arc.bytes_slice(), Bytecode::new_raw(good_code).bytes_slice()); - // Compile-time check that the return type is `Arc` (refcount share with cache). - let _arc_clone: Arc = Arc::clone(arc); + let code = ok.get(&good_hash).expect("good hash present"); + assert_eq!(code.bytes_slice(), Bytecode::new_raw(good_code).bytes_slice()); + // Compile-time check that the return type is plain `Bytecode` (a cheap refcount clone). + let _code_clone: Bytecode = code.clone(); handle.stop().unwrap(); } diff --git a/crates/stateless-core/src/db.rs b/crates/stateless-core/src/db.rs index 01942eb5..f75f2626 100644 --- a/crates/stateless-core/src/db.rs +++ b/crates/stateless-core/src/db.rs @@ -12,7 +12,7 @@ //! Concrete implementations live in their respective binaries; //! shared redb helpers live in the `stateless-db` crate. -use std::{boxed::Box, fmt, string::String, sync::Arc, vec::Vec}; +use std::{boxed::Box, fmt, string::String, vec::Vec}; use alloy_primitives::{B256, BlockHash, BlockNumber, map::HashMap}; use revm::state::Bytecode; @@ -86,16 +86,17 @@ impl fmt::Display for MissingDataKind { } /// Result of a contract bytecode lookup: `(found, missing)`. -pub type ContractLookup = (HashMap>, Vec); +pub type ContractLookup = (HashMap, Vec); /// Contract bytecode persistence. /// -/// Values are exchanged as `Arc` so the in-memory `ContractCache` and -/// downstream consumers (e.g. revm execution via `WitnessDatabase`) can share a -/// single allocation instead of deep-cloning bytecode on every cache hit. +/// Values are exchanged as plain `Bytecode`, not `Arc`: `Bytecode` is already +/// internally reference-counted (its `Bytes` buffer and `JumpTable` are both `Arc`-backed), +/// so cloning is an O(1) refcount bump that shares the same allocation. An outer `Arc` would +/// only add a redundant layer of indirection and a second heap allocation per contract. pub trait ContractStore: Send + Sync { fn get_contracts(&self, hashes: &[B256]) -> StoreResult; - fn add_contracts(&self, codes: &[(B256, Arc)]) -> StoreResult<()>; + fn add_contracts(&self, codes: &[(B256, Bytecode)]) -> StoreResult<()>; } /// Chain-cursor management — the storage surface the pipeline drives on **every** scenario. diff --git a/crates/stateless-core/src/evm_database.rs b/crates/stateless-core/src/evm_database.rs index 53de5553..abdf2f3e 100644 --- a/crates/stateless-core/src/evm_database.rs +++ b/crates/stateless-core/src/evm_database.rs @@ -7,7 +7,6 @@ use std::{ format, string::{String, ToString}, - sync::Arc, vec::Vec, }; @@ -67,10 +66,11 @@ pub struct WitnessDatabase<'a, W> { /// Compact witness containing state subset and cryptographic proofs pub witness: &'a W, /// Contract bytecode cache, pre-populated before execution starts. - /// Values are `Arc` so the cache and any cloned `BlockData` share a - /// single allocation; revm's trait still demands an owned `Bytecode`, so the - /// `DatabaseRef` impls deref-clone at the read boundary. - pub contracts: &'a HashMap>, + /// Values are plain `Bytecode`, which is already internally reference-counted, so the cache + /// and any cloned `BlockData` share one allocation without an outer `Arc`. revm's trait + /// demands an owned `Bytecode`, so the `DatabaseRef` impls clone (a cheap refcount bump) at + /// the read boundary. + pub contracts: &'a HashMap, } impl<'a, W> WitnessDatabase<'a, W> @@ -104,10 +104,7 @@ where _ => None, }) { Some(acc) => { - let code = acc - .codehash - .and_then(|hash| self.contracts.get(&hash)) - .map(|arc| (**arc).clone()); + let code = acc.codehash.and_then(|hash| self.contracts.get(&hash)).cloned(); Ok(Some(AccountInfo { balance: acc.balance, nonce: acc.nonce, @@ -128,7 +125,7 @@ where } self.contracts .get(&code_hash) - .map(|arc| (**arc).clone()) + .cloned() .ok_or_else(|| WitnessDatabaseError("Code not found".to_string())) } @@ -347,4 +344,34 @@ mod tests { assert!(err.0.contains("bad metadata for bucket 65536")); } + + /// `code_by_hash_ref` serves bytecode from the pre-populated contracts map: the empty-code + /// hash short-circuits to empty bytecode, a present hash returns the bytecode (a cheap clone + /// sharing the same underlying buffer), and an absent hash errors. revm normally reads code + /// inline via `basic_ref`'s `AccountInfo.code`, so this by-hash path is otherwise untested. + #[test] + fn code_by_hash_ref_serves_contracts_map() { + let header = Header::default(); + let witness = LightWitness { kvs: Default::default(), levels: Default::default() }; + + let code = Bytecode::new_raw(Bytes::from_static(&[0x60, 0x00, 0x60, 0x01])); + let hash = code.hash_slow(); + let mut contracts = HashMap::default(); + contracts.insert(hash, code.clone()); + + let db = WitnessDatabase { header: &header, witness: &witness, contracts: &contracts }; + + // Empty-code hash short-circuits to empty bytecode without touching the map. + assert!(db.code_by_hash_ref(KECCAK_EMPTY).unwrap().is_empty()); + + let got = db.code_by_hash_ref(hash).unwrap(); + // Correctness: the bytecode for the requested hash came back. + assert_eq!(got.bytes_slice(), code.bytes_slice()); + // Cheap-clone property: the returned value shares the same underlying allocation. + assert_eq!(got.bytes_slice().as_ptr(), code.bytes_slice().as_ptr()); + + // Absent hash errors. + let err = db.code_by_hash_ref(B256::from([0xAB; 32])).unwrap_err(); + assert!(err.0.contains("Code not found")); + } } diff --git a/crates/stateless-core/src/executor.rs b/crates/stateless-core/src/executor.rs index 40697a18..0717bea9 100644 --- a/crates/stateless-core/src/executor.rs +++ b/crates/stateless-core/src/executor.rs @@ -26,7 +26,7 @@ //! The module integrates with the Salt witness system for state reconstruction //! and uses Revm for transaction execution. -use std::{boxed::Box, collections::BTreeMap, fmt::Debug, sync::Arc, vec::Vec}; +use std::{boxed::Box, collections::BTreeMap, fmt::Debug, vec::Vec}; #[cfg(feature = "std")] use std::{io::Write, time::Instant}; @@ -469,7 +469,7 @@ pub fn validate_block( block: &B, salt_witness: SaltWitness, mpt_witness: MptWitness, - contracts: &HashMap>, + contracts: &HashMap, #[cfg(feature = "std")] writer: Option>, ) -> Result { // A block carrying only transaction hashes can't be replayed — fail fast before paying diff --git a/crates/stateless-core/src/pipeline/tests.rs b/crates/stateless-core/src/pipeline/tests.rs index 207f0a9e..df605f02 100644 --- a/crates/stateless-core/src/pipeline/tests.rs +++ b/crates/stateless-core/src/pipeline/tests.rs @@ -83,10 +83,10 @@ impl MockStore { } impl crate::ContractStore for MockStore { - fn get_contracts(&self, _: &[B256]) -> StoreResult<(HashMap>, Vec)> { + fn get_contracts(&self, _: &[B256]) -> StoreResult<(HashMap, Vec)> { Ok((HashMap::default(), vec![])) } - fn add_contracts(&self, _: &[(B256, Arc)]) -> StoreResult<()> { + fn add_contracts(&self, _: &[(B256, Bytecode)]) -> StoreResult<()> { Ok(()) } } diff --git a/crates/stateless-db/src/cache.rs b/crates/stateless-db/src/cache.rs index 7aa436f4..f8b3becd 100644 --- a/crates/stateless-db/src/cache.rs +++ b/crates/stateless-db/src/cache.rs @@ -44,20 +44,15 @@ impl Default for ContractCacheConfig { #[derive(Debug, Clone, Default)] pub(crate) struct BytecodeWeighter; -impl Weighter> for BytecodeWeighter { - fn weight(&self, _key: &B256, val: &Arc) -> u64 { +impl Weighter for BytecodeWeighter { + fn weight(&self, _key: &B256, val: &Bytecode) -> u64 { const ENTRY_OVERHEAD: u64 = 128; ENTRY_OVERHEAD + val.bytes_slice().len() as u64 } } -type MemoryCache = Cache< - B256, - Arc, - BytecodeWeighter, - RandomState, - DefaultLifecycle>, ->; +type MemoryCache = + Cache>; /// Snapshot of cache counters. #[derive(Debug, Clone, Copy, Default)] @@ -80,8 +75,9 @@ pub struct ContractCacheStats { /// Writes go to both disk and memory (write-through; disk first so a failed store /// write never leaves memory hotter than disk). /// -/// Values are stored as `Arc` so that hits — the hot path — return by -/// reference-count bump instead of deep-copying the bytecode. +/// Values are stored as plain `Bytecode`, which is already internally reference-counted, so hits +/// — the hot path — return by a cheap refcount bump that shares the same allocation, not a deep +/// copy. An outer `Arc` would be redundant indirection. pub struct ContractCache { memory: MemoryCache, store: Arc, @@ -120,14 +116,14 @@ impl ContractCache { /// caller should use a verified RPC fetch (`RpcClient::get_codes(.., verify=true)`) /// to populate the cache so all entries arrive pre-verified. pub fn get(&self, hashes: &[B256]) -> StoreResult { - let mut found: HashMap> = HashMap::default(); + let mut found: HashMap = HashMap::default(); found.reserve(hashes.len()); let mut not_in_memory = Vec::with_capacity(hashes.len()); for &hash in hashes { - if let Some(arc) = self.memory.get(&hash) { + if let Some(code) = self.memory.get(&hash) { self.hits.fetch_add(1, Ordering::Relaxed); - found.insert(hash, arc); + found.insert(hash, code); } else { self.misses.fetch_add(1, Ordering::Relaxed); not_in_memory.push(hash); @@ -140,8 +136,8 @@ impl ContractCache { let (from_disk, missing) = self.store.get_contracts(¬_in_memory)?; - for (hash, arc) in &from_disk { - self.memory.insert(*hash, arc.clone()); + for (hash, code) in &from_disk { + self.memory.insert(*hash, code.clone()); } found.extend(from_disk); @@ -149,15 +145,15 @@ impl ContractCache { } /// Adds contract bytecodes to both disk and memory (write-through). - pub fn insert(&self, codes: &[(B256, Arc)]) -> StoreResult<()> { + pub fn insert(&self, codes: &[(B256, Bytecode)]) -> StoreResult<()> { if codes.is_empty() { return Ok(()); } self.store.add_contracts(codes)?; - for (hash, arc) in codes { - self.memory.insert(*hash, arc.clone()); + for (hash, code) in codes { + self.memory.insert(*hash, code.clone()); } self.inserts.fetch_add(1, Ordering::Relaxed); @@ -185,12 +181,12 @@ mod tests { use super::*; - fn bc(b: u8) -> Arc { - Arc::new(Bytecode::new_raw(Bytes::from(vec![b]))) + fn bc(b: u8) -> Bytecode { + Bytecode::new_raw(Bytes::from(vec![b])) } - fn bc_sized(len: usize, fill: u8) -> Arc { - Arc::new(Bytecode::new_raw(Bytes::from(vec![fill; len]))) + fn bc_sized(len: usize, fill: u8) -> Bytecode { + Bytecode::new_raw(Bytes::from(vec![fill; len])) } fn h(n: u8) -> B256 { @@ -200,7 +196,7 @@ mod tests { /// In-memory `ContractStore` that tracks hit counts and can be toggled to fail on writes. #[derive(Default)] struct FakeStore { - data: dashmap::DashMap>, + data: dashmap::DashMap, get_calls: AtomicUsize, add_calls: AtomicUsize, fail_add: std::sync::atomic::AtomicBool, @@ -219,9 +215,9 @@ mod tests { fn get_contracts( &self, hashes: &[B256], - ) -> StoreResult<(HashMap>, Vec)> { + ) -> StoreResult<(HashMap, Vec)> { self.get_calls.fetch_add(1, Ordering::Relaxed); - let mut found: HashMap> = HashMap::default(); + let mut found: HashMap = HashMap::default(); let mut missing = Vec::new(); for &h in hashes { match self.data.get(&h) { @@ -234,7 +230,7 @@ mod tests { Ok((found, missing)) } - fn add_contracts(&self, codes: &[(B256, Arc)]) -> StoreResult<()> { + fn add_contracts(&self, codes: &[(B256, Bytecode)]) -> StoreResult<()> { self.add_calls.fetch_add(1, Ordering::Relaxed); if self.fail_add.load(Ordering::Relaxed) { return Err(StoreError::Corrupt("fake add failure".into())); @@ -255,7 +251,13 @@ mod tests { let (found, missing) = cache.get(&[h(1)]).unwrap(); let returned = found.get(&h(1)).unwrap(); - assert!(Arc::ptr_eq(returned, &original), "memory hit must share the same Arc allocation"); + // `Bytecode` is internally reference-counted, so a memory hit shares the same underlying + // buffer rather than deep-copying: the cloned-out value points at the same allocation. + assert_eq!( + returned.bytes_slice().as_ptr(), + original.bytes_slice().as_ptr(), + "memory hit must share the same bytecode allocation (cheap clone, no deep copy)", + ); assert!(missing.is_empty()); assert_eq!(store.get_calls(), 0, "memory hit must not hit store"); } diff --git a/crates/stateless-db/src/helpers.rs b/crates/stateless-db/src/helpers.rs index 9d2fcff0..374a32e1 100644 --- a/crates/stateless-db/src/helpers.rs +++ b/crates/stateless-db/src/helpers.rs @@ -1,7 +1,5 @@ //! Shared redb read/write helpers used by both concrete database implementations. -use std::sync::Arc; - use alloy_primitives::{B256, BlockHash, BlockNumber, map::HashMap}; use redb::{ReadableDatabase, ReadableTable}; use revm::state::Bytecode; @@ -150,14 +148,14 @@ pub fn read_contracts(database: &Database, hashes: &[B256]) -> StoreResult> = HashMap::default(); + let mut found: HashMap = HashMap::default(); let mut missing = Vec::new(); for &hash in hashes { match table.get(hash.0).store_err()? { Some(data) => { let bytecode: Bytecode = decode_from_slice(data.value().as_slice())?; - found.insert(hash, Arc::new(bytecode)); + found.insert(hash, bytecode); } None => missing.push(hash), } @@ -167,10 +165,7 @@ pub fn read_contracts(database: &Database, hashes: &[B256]) -> StoreResult)], -) -> StoreResult<()> { +pub fn write_add_contracts(database: &Database, codes: &[(B256, Bytecode)]) -> StoreResult<()> { if codes.is_empty() { return Ok(()); } @@ -178,7 +173,7 @@ pub fn write_add_contracts( { let mut table = write_txn.open_table(CONTRACTS).store_err()?; for (hash, bytecode) in codes { - let encoded = encode_to_vec(bytecode.as_ref())?; + let encoded = encode_to_vec(bytecode)?; table.insert(hash.0, encoded).store_err()?; } } @@ -264,8 +259,8 @@ mod tests { fn contracts_roundtrip_and_missing_report() { let (_dir, db) = temp_db(); - let a = (B256::from([1u8; 32]), Arc::new(Bytecode::new_raw(Bytes::from_static(&[0x60])))); - let b = (B256::from([2u8; 32]), Arc::new(Bytecode::new_raw(Bytes::from_static(&[0x61])))); + let a = (B256::from([1u8; 32]), Bytecode::new_raw(Bytes::from_static(&[0x60]))); + let b = (B256::from([2u8; 32]), Bytecode::new_raw(Bytes::from_static(&[0x61]))); let missing_hash = B256::from([3u8; 32]); write_add_contracts(&db, &[]).unwrap(); diff --git a/crates/stateless-test-utils/src/fixtures.rs b/crates/stateless-test-utils/src/fixtures.rs index 31494dad..98b20c07 100644 --- a/crates/stateless-test-utils/src/fixtures.rs +++ b/crates/stateless-test-utils/src/fixtures.rs @@ -13,7 +13,7 @@ use std::{ fs::File, io::{BufRead, BufReader}, path::{Path, PathBuf}, - sync::{Arc, LazyLock}, + sync::LazyLock, }; use alloy_genesis::Genesis; @@ -47,7 +47,7 @@ pub struct TestFixtures { pub block_numbers: BTreeMap, pub salt_witnesses: HashMap, pub mpt_witness_bytes: HashMap>, - pub contracts: HashMap>, + pub contracts: HashMap, } impl TestFixtures { @@ -184,7 +184,7 @@ pub fn load_json(path: impl AsRef) -> Result { } /// Loads contract bytecodes from a file (one `[hash, bytecode]` JSON per line). -pub fn load_contracts(path: impl AsRef) -> HashMap> { +pub fn load_contracts(path: impl AsRef) -> HashMap { let path = path.as_ref(); let file = File::open(path).unwrap_or_else(|e| panic!("open {}: {e}", path.display())); BufReader::new(file) @@ -194,7 +194,7 @@ pub fn load_contracts(path: impl AsRef) -> HashMap> { .map(|l| { let (hash, bytecode): (B256, Bytecode) = serde_json::from_str(&l).expect("parse contract"); - (hash, Arc::new(bytecode)) + (hash, bytecode) }) .collect() }