use revm::{
context_interface::context::SStoreResult,
primitives::{Address, AddressMap, HashMap, HashSet, StorageKey},
};
pub const PAGE_SHIFT: usize = 7;
pub const WORDS_PER_PAGE: u64 = 1 << PAGE_SHIFT;
pub const BASE_COST: u64 = 100;
pub const PAGE_WRITE_COST: u64 = 2_800;
pub const STATE_GROWTH_COST: u64 = 17_000;
#[inline]
pub fn page_index(slot: StorageKey) -> StorageKey {
slot >> PAGE_SHIFT
}
#[inline]
pub fn page_offset(slot: StorageKey) -> StorageKey {
slot & StorageKey::from(WORDS_PER_PAGE - 1)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct StoragePageKey {
pub address: Address,
pub page: StorageKey,
}
impl StoragePageKey {
#[inline]
pub fn from_slot(address: Address, slot: StorageKey) -> Self {
Self { address, page: page_index(slot) }
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PageAccessTracker {
read_accessed_pages: HashSet<StoragePageKey>,
write_accessed_pages: HashSet<StoragePageKey>,
current_state_growth: HashMap<StoragePageKey, i32>,
net_state_growth: HashMap<StoragePageKey, i32>,
change_journal: alloc::vec::Vec<PageTrackerChange>,
checkpoint_stack: alloc::vec::Vec<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PageTrackerChange {
ReadAccessed(StoragePageKey),
WriteAccessed(StoragePageKey),
CurrentStateGrowth { key: StoragePageKey, previous: i32, existed: bool },
NetStateGrowth { key: StoragePageKey, previous: i32, existed: bool },
}
impl PageAccessTracker {
#[inline]
pub fn is_read_accessed(&self, key: &StoragePageKey) -> bool {
self.read_accessed_pages.contains(key)
}
#[inline]
pub fn mark_read_accessed(&mut self, key: StoragePageKey) {
if self.read_accessed_pages.insert(key) {
self.change_journal.push(PageTrackerChange::ReadAccessed(key));
}
}
pub fn warm_access_list(&mut self, access_list: &AddressMap<HashSet<StorageKey>>) {
for (address, keys) in access_list {
for key in keys {
self.read_accessed_pages.insert(StoragePageKey::from_slot(*address, *key));
}
}
}
#[inline]
pub fn checkpoint(&mut self) {
self.checkpoint_stack.push(self.change_journal.len());
}
#[inline]
pub fn checkpoint_commit(&mut self) {
let _ = self.checkpoint_stack.pop();
}
pub fn checkpoint_revert(&mut self) {
let Some(journal_len) = self.checkpoint_stack.pop() else {
return;
};
while self.change_journal.len() > journal_len {
let change = self.change_journal.pop().expect("change journal length checked");
match change {
PageTrackerChange::ReadAccessed(key) => {
self.read_accessed_pages.remove(&key);
}
PageTrackerChange::WriteAccessed(key) => {
self.write_accessed_pages.remove(&key);
}
PageTrackerChange::CurrentStateGrowth { key, previous, existed } => {
restore_counter(&mut self.current_state_growth, key, previous, existed);
}
PageTrackerChange::NetStateGrowth { key, previous, existed } => {
restore_counter(&mut self.net_state_growth, key, previous, existed);
}
}
}
}
pub fn sstore_gas(&mut self, key: StoragePageKey, result: &SStoreResult) -> u64 {
let mut gas = BASE_COST;
if result.new_values_changes_present() && self.mark_write_accessed(key) {
gas += PAGE_WRITE_COST;
}
let growth_delta = match (result.is_present_zero(), result.is_new_zero()) {
(true, false) => 1,
(false, true) => -1,
_ => 0,
};
if growth_delta == 0 {
return gas;
}
let current_growth = self.current_state_growth(key) + growth_delta;
self.set_current_state_growth(key, current_growth);
if current_growth > self.net_state_growth(key) {
gas += STATE_GROWTH_COST;
self.set_net_state_growth(key, current_growth);
}
gas
}
pub fn clear(&mut self) {
self.read_accessed_pages.clear();
self.write_accessed_pages.clear();
self.current_state_growth.clear();
self.net_state_growth.clear();
self.change_journal.clear();
self.checkpoint_stack.clear();
}
#[inline]
fn mark_write_accessed(&mut self, key: StoragePageKey) -> bool {
if self.write_accessed_pages.insert(key) {
self.change_journal.push(PageTrackerChange::WriteAccessed(key));
true
} else {
false
}
}
#[inline]
fn current_state_growth(&self, key: StoragePageKey) -> i32 {
self.current_state_growth.get(&key).copied().unwrap_or_default()
}
#[inline]
fn net_state_growth(&self, key: StoragePageKey) -> i32 {
self.net_state_growth.get(&key).copied().unwrap_or_default()
}
fn set_current_state_growth(&mut self, key: StoragePageKey, value: i32) {
let previous = self.current_state_growth.get(&key).copied();
if previous == Some(value) || (previous.is_none() && value == 0) {
return;
}
self.change_journal.push(PageTrackerChange::CurrentStateGrowth {
key,
previous: previous.unwrap_or_default(),
existed: previous.is_some(),
});
set_counter(&mut self.current_state_growth, key, value);
}
fn set_net_state_growth(&mut self, key: StoragePageKey, value: i32) {
let previous = self.net_state_growth.get(&key).copied();
if previous == Some(value) || (previous.is_none() && value == 0) {
return;
}
self.change_journal.push(PageTrackerChange::NetStateGrowth {
key,
previous: previous.unwrap_or_default(),
existed: previous.is_some(),
});
set_counter(&mut self.net_state_growth, key, value);
}
}
fn set_counter(counters: &mut HashMap<StoragePageKey, i32>, key: StoragePageKey, value: i32) {
if value == 0 {
counters.remove(&key);
} else {
counters.insert(key, value);
}
}
fn restore_counter(
counters: &mut HashMap<StoragePageKey, i32>,
key: StoragePageKey,
previous: i32,
existed: bool,
) {
if existed {
counters.insert(key, previous);
} else {
counters.remove(&key);
}
}
#[cfg(test)]
mod tests {
use super::*;
use revm::primitives::{address, U256};
fn transition(original: u64, present: u64, new: u64) -> SStoreResult {
SStoreResult {
original_value: U256::from(original),
present_value: U256::from(present),
new_value: U256::from(new),
}
}
#[test]
fn page_math_matches_spec() {
let slot = U256::from(0x181);
assert_eq!(page_index(slot), U256::from(3));
assert_eq!(page_offset(slot), U256::from(1));
}
#[test]
fn every_sstore_charges_base_cost() {
let key = StoragePageKey::from_slot(Address::ZERO, U256::ZERO);
let mut tracker = PageAccessTracker::default();
assert_eq!(tracker.sstore_gas(key, &transition(0, 0, 0)), BASE_COST);
assert_eq!(tracker.sstore_gas(key, &transition(0, 1, 0)), BASE_COST + PAGE_WRITE_COST);
}
#[test]
fn first_new_slot_charges_write_and_growth() {
let key = StoragePageKey::from_slot(Address::ZERO, U256::ZERO);
let mut tracker = PageAccessTracker::default();
assert_eq!(
tracker.sstore_gas(key, &transition(0, 0, 1)),
BASE_COST + PAGE_WRITE_COST + STATE_GROWTH_COST
);
}
#[test]
fn clearing_existing_slot_offsets_new_growth_in_same_page() {
let key = StoragePageKey::from_slot(Address::ZERO, U256::ZERO);
let mut tracker = PageAccessTracker::default();
assert_eq!(tracker.sstore_gas(key, &transition(1, 1, 0)), BASE_COST + PAGE_WRITE_COST);
assert_eq!(tracker.sstore_gas(key, &transition(0, 0, 1)), BASE_COST);
}
#[test]
fn restoring_new_slot_does_not_recharge_growth() {
let key = StoragePageKey::from_slot(Address::ZERO, U256::ZERO);
let mut tracker = PageAccessTracker::default();
let _ = tracker.sstore_gas(key, &transition(0, 0, 1));
assert_eq!(tracker.sstore_gas(key, &transition(0, 1, 0)), BASE_COST);
assert_eq!(tracker.sstore_gas(key, &transition(0, 0, 2)), BASE_COST);
}
#[test]
fn checkpoint_revert_restores_all_page_state() {
let address = address!("1234567890123456789012345678901234567890");
let key = StoragePageKey::from_slot(address, U256::ZERO);
let mut tracker = PageAccessTracker::default();
tracker.checkpoint();
tracker.mark_read_accessed(key);
let _ = tracker.sstore_gas(key, &transition(0, 0, 1));
tracker.checkpoint_revert();
assert!(!tracker.is_read_accessed(&key));
assert_eq!(
tracker.sstore_gas(key, &transition(0, 0, 1)),
BASE_COST + PAGE_WRITE_COST + STATE_GROWTH_COST
);
}
}