use alloy_primitives::{Address, B256, Bytes, U256};
use crate::freshness::SlotChange;
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SlotDelta {
Add(U256),
Sub(U256),
}
impl SlotDelta {
pub fn apply(self, current: U256) -> U256 {
match self {
SlotDelta::Add(amount) => current.saturating_add(amount),
SlotDelta::Sub(amount) => current.saturating_sub(amount),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum StateUpdate {
Slot {
address: Address,
slot: U256,
value: U256,
},
SlotDelta {
address: Address,
slot: U256,
delta: SlotDelta,
},
BalanceDelta {
address: Address,
delta: SlotDelta,
},
SlotMasked {
address: Address,
slot: U256,
mask: U256,
value: U256,
},
Account {
address: Address,
patch: AccountPatch,
},
AccountUpsert {
address: Address,
patch: AccountPatch,
},
Purge {
address: Address,
scope: PurgeScope,
},
}
impl StateUpdate {
pub fn slot(address: Address, slot: U256, value: U256) -> Self {
Self::Slot {
address,
slot,
value,
}
}
pub fn slot_delta(address: Address, slot: U256, delta: SlotDelta) -> Self {
Self::SlotDelta {
address,
slot,
delta,
}
}
pub fn slot_masked(address: Address, slot: U256, mask: U256, value: U256) -> Self {
Self::SlotMasked {
address,
slot,
mask,
value,
}
}
pub fn balance_delta(address: Address, delta: SlotDelta) -> Self {
Self::BalanceDelta { address, delta }
}
pub fn balance(address: Address, value: U256) -> Self {
Self::Account {
address,
patch: AccountPatch::default().balance(value),
}
}
pub fn nonce(address: Address, nonce: u64) -> Self {
Self::Account {
address,
patch: AccountPatch::default().nonce(nonce),
}
}
pub fn code(address: Address, code: Bytes) -> Self {
Self::Account {
address,
patch: AccountPatch::default().code(code),
}
}
pub fn account(address: Address, patch: AccountPatch) -> Self {
Self::Account { address, patch }
}
pub fn account_upsert(address: Address, patch: AccountPatch) -> Self {
Self::AccountUpsert { address, patch }
}
pub fn purge(address: Address, scope: PurgeScope) -> Self {
Self::Purge { address, scope }
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct AccountPatch {
pub balance: Option<U256>,
pub nonce: Option<u64>,
pub code: Option<Bytes>,
}
impl AccountPatch {
pub fn balance(mut self, balance: U256) -> Self {
self.balance = Some(balance);
self
}
pub fn nonce(mut self, nonce: u64) -> Self {
self.nonce = Some(nonce);
self
}
pub fn code(mut self, code: Bytes) -> Self {
self.code = Some(code);
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum PurgeScope {
Account,
AllStorage,
Slots(Vec<U256>),
}
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct StateDiff {
pub slots: Vec<SlotChange>,
pub accounts: Vec<AccountChange>,
pub purged: Vec<PurgeRecord>,
pub skipped: Vec<SkippedDelta>,
pub skipped_balances: Vec<SkippedBalanceDelta>,
pub skipped_masks: Vec<SkippedMask>,
pub skipped_accounts: Vec<SkippedAccountPatch>,
}
impl StateDiff {
pub fn is_empty(&self) -> bool {
self.slots.is_empty() && self.accounts.is_empty() && self.purged.is_empty()
}
pub fn len(&self) -> usize {
self.slots.len() + self.accounts.len() + self.purged.len()
}
pub fn has_skipped(&self) -> bool {
!self.skipped.is_empty()
|| !self.skipped_balances.is_empty()
|| !self.skipped_masks.is_empty()
|| !self.skipped_accounts.is_empty()
}
pub fn skipped_len(&self) -> usize {
self.skipped.len()
+ self.skipped_balances.len()
+ self.skipped_masks.len()
+ self.skipped_accounts.len()
}
pub fn is_fully_applied(&self) -> bool {
!self.has_skipped()
}
pub fn merge(&mut self, other: StateDiff) {
self.slots.extend(other.slots);
self.accounts.extend(other.accounts);
self.purged.extend(other.purged);
self.skipped.extend(other.skipped);
self.skipped_balances.extend(other.skipped_balances);
self.skipped_masks.extend(other.skipped_masks);
self.skipped_accounts.extend(other.skipped_accounts);
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AccountChange {
pub address: Address,
pub balance: Option<(U256, U256)>,
pub nonce: Option<(u64, u64)>,
pub code_hash: Option<(B256, B256)>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PurgeRecord {
pub address: Address,
pub scope: PurgeScope,
pub slots_removed: usize,
pub account_removed: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SkippedDelta {
pub address: Address,
pub slot: U256,
pub delta: SlotDelta,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SkippedBalanceDelta {
pub address: Address,
pub delta: SlotDelta,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SkippedMask {
pub address: Address,
pub slot: U256,
pub mask: U256,
pub value: U256,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SkippedAccountPatch {
pub address: Address,
pub patch: AccountPatch,
}
#[cfg(test)]
mod tests {
use super::*;
fn addr(n: u8) -> Address {
Address::repeat_byte(n)
}
#[test]
fn account_patch_default_is_all_none() {
let p = AccountPatch::default();
assert_eq!(p.balance, None);
assert_eq!(p.nonce, None);
assert_eq!(p.code, None);
}
#[test]
fn account_patch_builders_compose() {
let p = AccountPatch::default()
.balance(U256::from(42))
.nonce(7)
.code(Bytes::from_static(&[0x60, 0x00]));
assert_eq!(p.balance, Some(U256::from(42)));
assert_eq!(p.nonce, Some(7));
assert_eq!(p.code, Some(Bytes::from_static(&[0x60, 0x00])));
}
#[test]
fn state_update_constructors_produce_expected_variants() {
let a = addr(0xaa);
assert_eq!(
StateUpdate::slot(a, U256::from(1), U256::from(2)),
StateUpdate::Slot {
address: a,
slot: U256::from(1),
value: U256::from(2),
}
);
assert_eq!(
StateUpdate::balance(a, U256::from(9)),
StateUpdate::Account {
address: a,
patch: AccountPatch::default().balance(U256::from(9)),
}
);
assert_eq!(
StateUpdate::account_upsert(a, AccountPatch::default().balance(U256::from(9))),
StateUpdate::AccountUpsert {
address: a,
patch: AccountPatch::default().balance(U256::from(9)),
}
);
assert_eq!(
StateUpdate::purge(a, PurgeScope::Account),
StateUpdate::Purge {
address: a,
scope: PurgeScope::Account,
}
);
}
#[test]
fn state_diff_default_is_empty() {
let d = StateDiff::default();
assert!(d.is_empty());
assert_eq!(d.len(), 0);
}
#[test]
fn state_diff_merge_concatenates_and_counts() {
let a = addr(0xbb);
let mut left = StateDiff::default();
left.slots.push(SlotChange {
address: a,
slot: U256::from(1),
old: U256::ZERO,
new: U256::from(5),
});
let mut right = StateDiff::default();
right.accounts.push(AccountChange {
address: a,
balance: Some((U256::ZERO, U256::from(3))),
nonce: None,
code_hash: None,
});
right.purged.push(PurgeRecord {
address: a,
scope: PurgeScope::AllStorage,
slots_removed: 2,
account_removed: false,
});
left.merge(right);
assert!(!left.is_empty());
assert_eq!(left.len(), 3);
assert_eq!(left.slots.len(), 1);
assert_eq!(left.accounts.len(), 1);
assert_eq!(left.purged.len(), 1);
assert_eq!(left.slots[0].new, U256::from(5));
}
#[test]
fn slot_delta_add_applies_saturating() {
assert_eq!(
SlotDelta::Add(U256::from(50)).apply(U256::from(100)),
U256::from(150)
);
assert_eq!(
SlotDelta::Add(U256::from(10)).apply(U256::MAX - U256::from(1)),
U256::MAX
);
assert_eq!(SlotDelta::Add(U256::from(5)).apply(U256::MAX), U256::MAX);
}
#[test]
fn slot_delta_sub_applies_saturating() {
assert_eq!(
SlotDelta::Sub(U256::from(30)).apply(U256::from(100)),
U256::from(70)
);
assert_eq!(
SlotDelta::Sub(U256::from(50)).apply(U256::from(30)),
U256::ZERO
);
assert_eq!(SlotDelta::Sub(U256::from(1)).apply(U256::ZERO), U256::ZERO);
}
#[test]
fn state_update_slot_delta_constructor() {
let a = addr(0xcc);
assert_eq!(
StateUpdate::slot_delta(a, U256::from(1), SlotDelta::Add(U256::from(2))),
StateUpdate::SlotDelta {
address: a,
slot: U256::from(1),
delta: SlotDelta::Add(U256::from(2)),
}
);
}
#[test]
fn state_diff_merge_extends_skipped_without_counting_it() {
let a = addr(0xdd);
let mut left = StateDiff::default();
let mut right = StateDiff::default();
right.skipped.push(SkippedDelta {
address: a,
slot: U256::from(1),
delta: SlotDelta::Sub(U256::from(3)),
});
left.merge(right);
assert_eq!(left.skipped.len(), 1);
assert!(left.is_empty());
assert_eq!(left.len(), 0);
}
#[test]
fn slot_masked_constructor_produces_variant() {
let a = addr(0xee);
assert_eq!(
StateUpdate::slot_masked(a, U256::from(1), U256::from(0xFF), U256::from(0x42)),
StateUpdate::SlotMasked {
address: a,
slot: U256::from(1),
mask: U256::from(0xFF),
value: U256::from(0x42),
}
);
}
#[test]
fn state_diff_merge_extends_skipped_masks_without_counting_it() {
let a = addr(0xef);
let mut left = StateDiff::default();
let mut right = StateDiff::default();
right.skipped_masks.push(SkippedMask {
address: a,
slot: U256::from(1),
mask: U256::from(0xFF),
value: U256::from(0x42),
});
left.merge(right);
assert_eq!(left.skipped_masks.len(), 1);
assert!(left.is_empty());
assert_eq!(left.len(), 0);
assert!(left.has_skipped());
assert_eq!(left.skipped_len(), 1);
assert!(!left.is_fully_applied());
}
#[test]
fn slot_masked_serde_round_trips() {
let a = addr(0xf0);
let update = StateUpdate::slot_masked(a, U256::from(5), U256::from(0xFF), U256::from(3));
let json = serde_json::to_string(&update).expect("serialize");
let back: StateUpdate = serde_json::from_str(&json).expect("deserialize");
assert_eq!(update, back);
let mask = SkippedMask {
address: a,
slot: U256::from(1),
mask: U256::from(0xFF),
value: U256::from(2),
};
let json = serde_json::to_string(&mask).expect("serialize");
let back: SkippedMask = serde_json::from_str(&json).expect("deserialize");
assert_eq!(mask, back);
}
}