diff --git a/Cargo.lock b/Cargo.lock index 08c9012722..0440d3ec43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3752,6 +3752,7 @@ dependencies = [ "revm-primitives", "revm-state", "serde", + "serde_json", ] [[package]] diff --git a/crates/context/Cargo.toml b/crates/context/Cargo.toml index a507d7b129..7f475af76f 100644 --- a/crates/context/Cargo.toml +++ b/crates/context/Cargo.toml @@ -28,13 +28,14 @@ bytecode.workspace = true # misc derive-where.workspace = true cfg-if.workspace = true -bitvec.workspace = true # Optional serde = { workspace = true, features = ["derive", "rc"], optional = true } [dev-dependencies] database.workspace = true +bitvec.workspace = true +serde_json.workspace = true [features] default = ["std"] @@ -47,6 +48,7 @@ std = [ "primitives/std", "state/std", "bitvec/std", + "serde_json/std" ] serde = [ "dep:serde", @@ -57,7 +59,7 @@ serde = [ "database/serde", "database-interface/serde", "derive-where/serde", - "bitvec/serde", + "bitvec/serde" ] dev = [ "memory_limit", diff --git a/crates/context/src/journal/warm_addresses.rs b/crates/context/src/journal/warm_addresses.rs index 60a444c1df..862d267e13 100644 --- a/crates/context/src/journal/warm_addresses.rs +++ b/crates/context/src/journal/warm_addresses.rs @@ -2,12 +2,14 @@ //! //! It is used to optimize access to precompile addresses. -use bitvec::{bitvec, vec::BitVec}; use context_interface::journaled_state::JournalLoadError; use primitives::{ short_address, Address, AddressMap, AddressSet, HashSet, StorageKey, SHORT_ADDRESS_CAP, }; +/// Number of bytes needed to hold SHORT_ADDRESS_CAP bits (300 bits == 38 bytes). +const PRECOMPILE_SHORT_ADDRESS_BYTES: usize = SHORT_ADDRESS_CAP.div_ceil(8); + /// Stores addresses that are warm loaded. Contains precompiles and coinbase address. /// /// It contains precompiles addresses that are not changed frequently and AccessList that @@ -22,9 +24,11 @@ use primitives::{ pub struct WarmAddresses { /// Set of warm loaded precompile addresses. precompile_set: AddressSet, - /// Bit vector of precompile short addresses. If address is shorter than [`SHORT_ADDRESS_CAP`] it - /// will be stored in this bit vector for faster access. - precompile_short_addresses: BitVec, + /// Fixed bitset of precompile short addresses (one bit per possible short address). + /// Uses a plain byte array + manual bit ops for fast access in the hot path + /// (avoids BitVec overhead, similar to JumpTable optimization). + #[cfg_attr(feature = "serde", serde(with = "fixed_array"))] + precompile_short_addresses: [u8; PRECOMPILE_SHORT_ADDRESS_BYTES], /// `true` if all precompiles are short addresses. precompile_all_short_addresses: bool, /// Coinbase address. @@ -45,7 +49,7 @@ impl WarmAddresses { pub fn new() -> Self { Self { precompile_set: AddressSet::default(), - precompile_short_addresses: bitvec![0; SHORT_ADDRESS_CAP], + precompile_short_addresses: [0u8; PRECOMPILE_SHORT_ADDRESS_BYTES], precompile_all_short_addresses: true, coinbase: None, access_list: AddressMap::default(), @@ -66,12 +70,12 @@ impl WarmAddresses { /// Set the precompile addresses and short addresses. pub fn set_precompile_addresses(&mut self, addresses: &AddressSet) { - self.precompile_short_addresses.fill(false); + self.precompile_short_addresses.fill(0); let mut all_short_addresses = true; for address in addresses.iter() { if let Some(short_address) = short_address(address) { - self.precompile_short_addresses.set(short_address, true); + self.set_precompile_short(short_address); } else { all_short_addresses = false; } @@ -81,6 +85,39 @@ impl WarmAddresses { self.precompile_set.clone_from(addresses); } + /// Set the bit for a short precompile address. + /// + /// Uses manual bit manipulation on a fixed byte array (instead of `BitVec`) + /// to avoid per-access overhead + #[inline(always)] + const fn set_precompile_short(&mut self, idx: usize) { + debug_assert!( + idx < SHORT_ADDRESS_CAP, + "Index out of bounds for short address" + ); + // get the index to the byte in the arr + let byte = idx >> 3; + // bit position within the byte (0..7) + let bit = 1 << (idx & 7); + self.precompile_short_addresses[byte] |= bit; + } + + /// Returns whether the bit for the given short address index is set. + /// + /// This is the hot path inside `is_warm`/`is_cold` for short precompile + /// addresses. We perform direct byte + bit operations on the fixed array + /// instead of going through `BitVec` (same technique I used in `JumpTable::is_valid`). + #[inline(always)] + pub(crate) const fn is_precompile_short(&self, idx: usize) -> bool { + debug_assert!( + idx < SHORT_ADDRESS_CAP, + "Index out of bounds for short address" + ); + let byte = idx >> 3; + let bit = 1 << (idx & 7); + self.precompile_short_addresses[byte] & bit != 0 + } + /// Set the coinbase address. #[inline] pub const fn set_coinbase(&mut self, address: Address) { @@ -131,7 +168,7 @@ impl WarmAddresses { // check if it is short precompile address if let Some(short_address) = short_address(address) { - return self.precompile_short_addresses[short_address]; + return self.is_precompile_short(short_address); } if !self.precompile_all_short_addresses { @@ -174,6 +211,92 @@ impl WarmAddresses { } } +#[cfg(feature = "serde")] +mod fixed_array { + extern crate alloc; + use serde::de::{value::MapAccessDeserializer, Error, MapAccess, SeqAccess, Visitor}; + use serde::{Deserialize, Deserializer, Serializer}; + + pub(super) fn serialize( + arr: &[u8; N], + + serializer: S, + ) -> Result { + serializer.serialize_bytes(arr) + } + + #[derive(Deserialize)] + + struct LegacyBitVec { + #[allow(dead_code)] + order: serde::de::IgnoredAny, + #[allow(dead_code)] + head: LegacyHead, + #[allow(dead_code)] + bits: usize, + data: alloc::vec::Vec, + } + + #[derive(Deserialize)] + + struct LegacyHead { + #[allow(dead_code)] + width: usize, + #[allow(dead_code)] + index: usize, + } + + pub(super) fn deserialize<'de, D: Deserializer<'de>, const N: usize>( + deserializer: D, + ) -> Result<[u8; N], D::Error> { + struct FixedArrayVisitor; + + impl<'de, const N: usize> Visitor<'de> for FixedArrayVisitor { + type Value = [u8; N]; + + fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "byte array or legacy BitVec") + } + + fn visit_bytes(self, v: &[u8]) -> Result<[u8; N], E> { + v.try_into() + .map_err(|_| E::custom("invalid byte array length")) + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result<[u8; N], E> { + self.visit_bytes(v) + } + + fn visit_seq>(self, mut seq: A) -> Result<[u8; N], A::Error> { + let mut arr = [0; N]; + + for b in &mut arr { + *b = seq + .next_element()? + .ok_or_else(|| A::Error::custom("invalid byte array length"))?; + } + + if seq.next_element::()?.is_some() { + return Err(A::Error::custom("invalid byte array length")); + } + + Ok(arr) + } + + fn visit_map>(self, map: A) -> Result<[u8; N], A::Error> { + let mut data = LegacyBitVec::deserialize(MapAccessDeserializer::new(map))?.data; + + data.resize(N, 0); + + data.try_into() + .map_err(|_| A::Error::custom("legacy BitVec exceeds fixed array size")) + } + } + + deserializer.deserialize_any(FixedArrayVisitor::) + } +} + #[cfg(test)] mod tests { use super::*; @@ -185,9 +308,12 @@ mod tests { assert!(warm_addresses.precompile_set.is_empty()); assert_eq!( warm_addresses.precompile_short_addresses.len(), - SHORT_ADDRESS_CAP + PRECOMPILE_SHORT_ADDRESS_BYTES ); - assert!(!warm_addresses.precompile_short_addresses.any()); + assert!(warm_addresses + .precompile_short_addresses + .iter() + .all(|&b| b == 0)); assert!(warm_addresses.coinbase.is_none()); // Test Default trait @@ -234,13 +360,13 @@ mod tests { assert_eq!(warm_addresses.precompile_set, precompiles); assert_eq!( warm_addresses.precompile_short_addresses.len(), - SHORT_ADDRESS_CAP + PRECOMPILE_SHORT_ADDRESS_BYTES ); - // Verify bitvec optimization - assert!(warm_addresses.precompile_short_addresses[1]); - assert!(warm_addresses.precompile_short_addresses[5]); - assert!(!warm_addresses.precompile_short_addresses[0]); + // Verify optimization (replaces old BitVec) + assert!(warm_addresses.is_precompile_short(1)); + assert!(warm_addresses.is_precompile_short(5)); + assert!(!warm_addresses.is_precompile_short(0)); // Verify warmth detection assert!(warm_addresses.is_warm(&short_addr1)); @@ -272,7 +398,10 @@ mod tests { // Verify storage assert_eq!(warm_addresses.precompile_set, precompiles); - assert!(!warm_addresses.precompile_short_addresses.any()); + assert!(warm_addresses + .precompile_short_addresses + .iter() + .all(|&b| b == 0)); // Verify warmth detection assert!(warm_addresses.is_warm(®ular_addr)); @@ -303,8 +432,8 @@ mod tests { assert!(warm_addresses.is_warm(®ular_addr)); // Verify short address optimization is used - assert!(warm_addresses.precompile_short_addresses[7]); - assert!(!warm_addresses.precompile_short_addresses[8]); + assert!(warm_addresses.is_precompile_short(7)); + assert!(!warm_addresses.is_precompile_short(8)); } #[test] @@ -324,6 +453,260 @@ mod tests { warm_addresses.set_precompile_addresses(&precompiles); assert!(warm_addresses.is_warm(&boundary_addr)); - assert!(warm_addresses.precompile_short_addresses[SHORT_ADDRESS_CAP - 1]); + assert!(warm_addresses.is_precompile_short(SHORT_ADDRESS_CAP - 1)); + } + + #[cfg(feature = "serde")] + #[test] + fn test_serde_roundtrip() { + let mut warm_addresses = WarmAddresses::new(); + + // Create short and non-short precompiles + let mut short_bytes = [0u8; 20]; + short_bytes[19] = 1u8; + let short_addr = Address::from(short_bytes); + + let regular_addr = address!("1234567890123456789012345678901234567890"); + + let mut precompiles = HashSet::default(); + precompiles.insert(short_addr); + precompiles.insert(regular_addr); + + warm_addresses.set_precompile_addresses(&precompiles); + + let coinbase_addr = address!("0000000000000000000000000000000000000001"); + warm_addresses.set_coinbase(coinbase_addr); + + // Set access list with a storage slot + let mut access_list = AddressMap::default(); + let mut slots = HashSet::default(); + slots.insert(primitives::U256::from(42)); + access_list.insert(regular_addr, slots); + warm_addresses.set_access_list(access_list); + + let serialized = serde_json::to_string(&warm_addresses).expect("Failed to serialize"); + + let deserialized: WarmAddresses = + serde_json::from_str(&serialized).expect("Failed to deserialize"); + + assert_eq!(warm_addresses, deserialized); + + assert!(deserialized.is_warm(&short_addr)); + assert!(deserialized.is_warm(®ular_addr)); + assert!(deserialized.is_warm(&coinbase_addr)); + } + + #[cfg(feature = "serde")] + #[test] + fn test_warm_addresses_deserializes_legacy_bitvec_format() { + use primitives::Address; + + let short_addr = Address::with_last_byte(1); + let mut warm = WarmAddresses::new(); + let mut precompiles = AddressSet::default(); + precompiles.insert(short_addr); + warm.set_precompile_addresses(&precompiles); + + assert!(warm.is_warm(&short_addr)); + + let serialized = serde_json::to_string(&warm).unwrap(); + + // Ensure we're not emitting legacy object form anymore + assert!(!serialized.contains(r#""order""#)); + + let modern: WarmAddresses = serde_json::from_str(&serialized).unwrap(); + + assert!(modern.is_warm(&short_addr)); + + // Historical BitVec format + let legacy_json = r#" + { + "precompile_set":["0x0000000000000000000000000000000000000001"], + "precompile_short_addresses":{ + "order":"bitvec::order::Lsb0", + "head":{"width":8,"index":0}, + "bits":300, + "data":[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + }, + "precompile_all_short_addresses":true, + "coinbase":null, + "access_list":{} + } + "#; + + let legacy: WarmAddresses = serde_json::from_str(legacy_json).unwrap(); + assert!(legacy.is_warm(&short_addr)); + assert!(legacy.is_precompile_short(1)); + + let reserialized = serde_json::to_string(&legacy).unwrap(); + assert!(!reserialized.contains(r#""order""#)); + } +} + +#[cfg(test)] +mod bench_is_short_precompile { + use super::*; + use std::time::Instant; + + use bitvec::{bitvec, vec::BitVec}; + + const ITERATIONS: usize = 1_000_000; + const TEST_SIZE: usize = SHORT_ADDRESS_CAP; + + /// Legacy BitVec version. + #[derive(Clone)] + struct ShortAddressesWithBitVec { + bits: BitVec, + } + + impl ShortAddressesWithBitVec { + fn new() -> Self { + Self { + bits: bitvec![0; SHORT_ADDRESS_CAP], + } + } + + fn set_many(&mut self, indices: impl IntoIterator) { + for i in indices { + if i < SHORT_ADDRESS_CAP { + self.bits.set(i, true); + } + } + } + + /// Old BitVec [] style (for comparison). + #[inline] + fn is_set(&self, idx: usize) -> bool { + self.bits[idx] + } + } + + fn create_test_data() -> (WarmAddresses, ShortAddressesWithBitVec) { + let mut real = WarmAddresses::new(); + let mut precompile_set = AddressSet::default(); + + for i in (0..TEST_SIZE).step_by(3) { + let mut bytes = [0u8; 20]; + bytes[18] = (i >> 8) as u8; + bytes[19] = i as u8; + precompile_set.insert(Address::from(bytes)); + } + real.set_precompile_addresses(&precompile_set); + + let mut legacy = ShortAddressesWithBitVec::new(); + legacy.set_many((0..TEST_SIZE).step_by(3)); + + (real, legacy) + } + + fn benchmark_implementation(name: &str, table: &F, test_fn: impl Fn(&F, usize) -> bool) + where + F: Clone, + { + for i in 0..10_000 { + std::hint::black_box(test_fn(table, i % TEST_SIZE)); + } + + let start = Instant::now(); + let mut count = 0; + + for i in 0..ITERATIONS { + if test_fn(table, i % TEST_SIZE) { + count += 1; + } + } + + let duration = start.elapsed(); + let ns_per_op = duration.as_nanos() as f64 / ITERATIONS as f64; + let ops_per_sec = ITERATIONS as f64 / duration.as_secs_f64(); + + println!("{name} Performance:"); + println!(" Time per op: {ns_per_op:.2} ns"); + println!(" Ops per sec: {ops_per_sec:.0}"); + println!(" True count: {count}"); + println!(); + + std::hint::black_box(count); + } + + #[test] + fn bench_is_short_precompile() { + println!("\nWarmAddresses short precompile bit test Benchmark Comparison"); + println!("============================================================"); + + let (real_warm, legacy_bitvec) = create_test_data(); + + benchmark_implementation( + "WarmAddresses (Fixed Array + Manual Bits)", + &real_warm, + |wa, idx| wa.is_precompile_short(idx), + ); + + benchmark_implementation("Legacy (BitVec indexing)", &legacy_bitvec, |lb, idx| { + lb.is_set(idx) + }); + + println!("Benchmark completed successfully!\n"); + } + + #[test] + fn bench_different_access_patterns() { + let (real_warm, legacy_bitvec) = create_test_data(); + + println!("Short Precompile Access Pattern Comparison"); + println!("=========================================="); + + let start = Instant::now(); + for i in 0..ITERATIONS { + std::hint::black_box(real_warm.is_precompile_short(i % TEST_SIZE)); + } + let fixed_sequential = start.elapsed(); + + let start = Instant::now(); + for i in 0..ITERATIONS { + std::hint::black_box(legacy_bitvec.is_set(i % TEST_SIZE)); + } + let bitvec_sequential = start.elapsed(); + + let start = Instant::now(); + for i in 0..ITERATIONS { + std::hint::black_box(real_warm.is_precompile_short((i * 17) % TEST_SIZE)); + } + let fixed_random = start.elapsed(); + + let start = Instant::now(); + for i in 0..ITERATIONS { + std::hint::black_box(legacy_bitvec.is_set((i * 17) % TEST_SIZE)); + } + let bitvec_random = start.elapsed(); + + println!("Sequential Access:"); + println!( + " Fixed Array: {:.2} ns/op", + fixed_sequential.as_nanos() as f64 / ITERATIONS as f64 + ); + println!( + " BitVec: {:.2} ns/op", + bitvec_sequential.as_nanos() as f64 / ITERATIONS as f64 + ); + println!( + " Speedup: {:.1}x", + bitvec_sequential.as_nanos() as f64 / fixed_sequential.as_nanos() as f64 + ); + + println!(); + println!("Random Access:"); + println!( + " Fixed Array: {:.2} ns/op", + fixed_random.as_nanos() as f64 / ITERATIONS as f64 + ); + println!( + " BitVec: {:.2} ns/op", + bitvec_random.as_nanos() as f64 / ITERATIONS as f64 + ); + println!( + " Speedup: {:.1}x", + bitvec_random.as_nanos() as f64 / fixed_random.as_nanos() as f64 + ); } }