Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions crates/stateless-core/src/data_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
use std::{collections::BTreeMap, vec::Vec};

pub use alloy_primitives::Bytes;
use alloy_primitives::{Address, B256, U256};
use alloy_primitives::{Address, B256, FixedBytes, U256};
use revm::primitives::KECCAK_EMPTY;
use salt::{SaltKey, SaltValue};

Expand Down Expand Up @@ -67,14 +67,33 @@ impl PlainKey {
/// - Unknown: preserved raw bytes from decode
pub fn encode(&self) -> Vec<u8> {
match self {
PlainKey::Account(addr) => addr.as_slice().to_vec(),
PlainKey::Storage(addr, slot) => {
addr.concat_const::<SLOT_KEY_LEN, STORAGE_SLOT_KEY_LEN>(*slot).as_slice().to_vec()
}
PlainKey::Account(addr) => Self::account_key_bytes(addr).to_vec(),
PlainKey::Storage(addr, slot) => Self::storage_key_bytes(*addr, *slot).to_vec(),
PlainKey::Unknown(data) => data.clone(),
}
}

/// Encoding of an account key — the raw address bytes.
///
/// Same bytes as `PlainKey::Account(address).encode()` without the heap allocation,
/// for per-state-read hot paths.
#[inline]
pub(crate) fn account_key_bytes(address: &Address) -> &[u8] {
address.as_slice()
}

/// Stack-allocated encoding of a storage-slot key — address (20) ++ slot (32).
///
/// Same bytes as `PlainKey::Storage(address, slot).encode()` without the heap
/// allocation, for per-state-read hot paths.
#[inline]
pub(crate) fn storage_key_bytes(
address: Address,
slot: B256,
) -> FixedBytes<STORAGE_SLOT_KEY_LEN> {
address.concat_const::<SLOT_KEY_LEN, STORAGE_SLOT_KEY_LEN>(slot)
}

/// Decodes a byte slice into a PlainKey.
///
/// Returns `PlainKey::Unknown` if the buffer length is neither 20 (account)
Expand Down Expand Up @@ -239,6 +258,22 @@ mod tests {
entries.into_iter().enumerate().map(|(i, v)| (SaltKey::from((0u32, i as u64)), v)).collect()
}

/// The allocation-free encoders the witness read path uses must produce exactly what
/// `encode()` produces — `encode()` delegates to them today, and this pins that contract
/// against a future edit that stops delegating and lets the two drift into looking up
/// different keys.
#[test]
fn stack_key_encodings_match_encode() {
let addr = Address::from([0x11; 20]);
assert_eq!(PlainKey::account_key_bytes(&addr), PlainKey::Account(addr).encode().as_slice());
for slot in [B256::ZERO, B256::from([0xAB; 32]), B256::from(U256::from(7))] {
assert_eq!(
PlainKey::storage_key_bytes(addr, slot).as_slice(),
PlainKey::Storage(addr, slot).encode().as_slice(),
);
}
}

#[test]
fn test_plain_key_round_trip() {
let addr = Address::from([0xAB; 20]);
Expand Down
28 changes: 17 additions & 11 deletions crates/stateless-core/src/evm_database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use std::{
collections::BTreeMap,
format,
string::{String, ToString},
vec::Vec,
};

use alloy_consensus::Header;
Expand Down Expand Up @@ -79,10 +78,16 @@ where
W: StateReader,
W::Error: core::fmt::Display,
{
/// Get value from witness for the given plain key
fn plain_value(&self, plain_key: &[u8]) -> Result<Option<Vec<u8>>, WitnessDatabaseError> {
/// Get the witness entry for the given plain key.
///
/// Returns the whole `SaltValue` (an inline array) instead of going through salt's
/// `plain_value()`, which heap-allocates a copy of the value bytes on every read —
/// this lookup runs once per unique account/slot touched during block replay.
/// Callers decode from `.value()` in place.
fn find(&self, plain_key: &[u8]) -> Result<Option<SaltValue>, WitnessDatabaseError> {
EphemeralSaltState::new(self.witness)
.plain_value(plain_key)
.find(plain_key)
.map(|found| found.map(|(_, salt_value)| salt_value))
Comment on lines 88 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the indexed witness lookup path

When W is LightWitnessExecutor in the debug-trace server, the old plain_value path can use StateReader::plain_value_fast, which is backed by the prebuilt direct_lookup_tbl in light_witness.rs; invoking EphemeralSaltState::find directly bypasses that index and probes the SALT table for every account and slot read. On trace replays with many state reads, this discards the executor's purpose-built fast path and may outweigh the saved value allocation. Resolve the SaltKey through plain_value_fast and read the SaltValue directly, retaining the find fallback needed for non-existent keys.

Useful? React with 👍 / 👎.

.map_err(|e| WitnessDatabaseError(e.to_string()))
}
}
Expand All @@ -98,9 +103,9 @@ where
fn basic_ref(&self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
trace!(?address, "basic_ref");

let raw_value = self.plain_value(&PlainKey::Account(address).encode())?;
let salt_value = self.find(PlainKey::account_key_bytes(&address))?;

match raw_value.and_then(|v| match PlainValue::decode(&v) {
match salt_value.and_then(|v| match PlainValue::decode(v.value()) {
PlainValue::Account(acc) => Some(acc),
_ => None,
}) {
Expand Down Expand Up @@ -134,10 +139,11 @@ where
fn storage_ref(&self, address: Address, index: U256) -> Result<U256, Self::Error> {
trace!(?address, index = %format_args!("{:#x}", index), "storage_ref");

let raw_value = self.plain_value(&PlainKey::Storage(address, index.into()).encode())?;
let salt_value =
self.find(PlainKey::storage_key_bytes(address, index.into()).as_slice())?;

Ok(raw_value
.and_then(|v| match PlainValue::decode(&v) {
Ok(salt_value
.and_then(|v| match PlainValue::decode(v.value()) {
PlainValue::Storage(value) => Some(value),
_ => None,
})
Expand Down Expand Up @@ -298,11 +304,11 @@ impl SaltEnv for WitnessExternalEnv {
}

fn bucket_id_for_account(account: Address) -> BucketId {
hasher::bucket_id(&PlainKey::Account(account).encode())
hasher::bucket_id(PlainKey::account_key_bytes(&account))
}

fn bucket_id_for_slot(address: Address, key: U256) -> BucketId {
hasher::bucket_id(&PlainKey::Storage(address, key.into()).encode())
hasher::bucket_id(PlainKey::storage_key_bytes(address, key.into()).as_slice())
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/stateless-core/src/light_witness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ impl StateReader for LightWitness {
fn metadata(&self, bucket_id: BucketId) -> Result<BucketMeta, Self::Error> {
let metadata_key = bucket_metadata_key(bucket_id);
match self.kvs.get(&metadata_key) {
Some(Some(salt_value)) => BucketMeta::try_from(salt_value.clone())
Some(Some(salt_value)) => BucketMeta::try_from(salt_value)
.map_err(|_| LightWitnessError { message: "Failed to decode metadata" }),
// A well-formed witness never maps a metadata key to a deletion,
// but witness bytes are network input (and the light decode
Expand Down
Loading