Skip to main content

evm_state_diff/
diff.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::Serialize;
4
5use crate::primitives::{Address, B256, Bytes, Delta, EMPTY_CODE_HASH, U256};
6use crate::trace::{AccountState, PrestateDiff, TxTrace};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
9pub struct Change<T> {
10    pub from: T,
11    pub to: T,
12}
13
14impl<T: PartialEq> Change<T> {
15    fn new(from: T, to: T) -> Option<Self> {
16        (from != to).then_some(Self { from, to })
17    }
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
21#[serde(rename_all = "lowercase")]
22pub enum AccountStatus {
23    /// The account did not exist (was empty) before.
24    Created,
25    /// The account was removed (self-destructed).
26    Destroyed,
27    Modified,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
31pub struct AccountDiff {
32    pub status: AccountStatus,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub balance: Option<Change<U256>>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub nonce: Option<Change<u64>>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub code: Option<Change<Bytes>>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub code_hash: Option<Change<B256>>,
41    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
42    pub storage: BTreeMap<B256, Change<B256>>,
43}
44
45impl AccountDiff {
46    fn empty(status: AccountStatus) -> Self {
47        Self { status, balance: None, nonce: None, code: None, code_hash: None, storage: BTreeMap::new() }
48    }
49
50    pub fn has_changes(&self) -> bool {
51        self.balance.is_some()
52            || self.nonce.is_some()
53            || self.code.is_some()
54            || self.code_hash.is_some()
55            || !self.storage.is_empty()
56    }
57
58    pub fn balance_delta(&self) -> Option<Delta> {
59        self.balance.map(|c| Delta::between(c.from, c.to))
60    }
61}
62
63/// Net state changes, keyed by account.
64#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
65#[serde(transparent)]
66pub struct StateDiff {
67    pub accounts: BTreeMap<Address, AccountDiff>,
68}
69
70#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
71pub struct Summary {
72    pub accounts: usize,
73    pub created: usize,
74    pub destroyed: usize,
75    pub balance_changes: usize,
76    pub nonce_changes: usize,
77    pub code_changes: usize,
78    pub storage_changes: usize,
79    /// Sum of all balance increases, in wei.
80    pub total_credited: U256,
81    /// Sum of all balance decreases, in wei.
82    pub total_debited: U256,
83}
84
85fn balance_of(a: Option<&AccountState>) -> U256 {
86    a.and_then(|a| a.balance).unwrap_or_default()
87}
88
89fn nonce_of(a: Option<&AccountState>) -> u64 {
90    a.and_then(|a| a.nonce).unwrap_or(0)
91}
92
93fn code_of(a: Option<&AccountState>) -> Bytes {
94    a.and_then(|a| a.code.clone()).unwrap_or_default()
95}
96
97fn code_hash_of(a: Option<&AccountState>) -> B256 {
98    a.and_then(|a| a.code_hash).unwrap_or(EMPTY_CODE_HASH)
99}
100
101impl StateDiff {
102    /// Reconstructs explicit before/after values from geth's diff-mode
103    /// encoding:
104    ///
105    /// * `post` lists only changed fields, so a missing field is unchanged;
106    /// * an account only in `post` was empty before (created);
107    /// * an account only in `pre` was deleted (self-destructed);
108    /// * a slot in `pre` but missing from a present `post` was cleared to zero;
109    /// * a slot only in `post` was zero before.
110    pub fn from_prestate(trace: &PrestateDiff) -> Self {
111        let addresses: BTreeSet<&Address> = trace.pre.keys().chain(trace.post.keys()).collect();
112        let mut accounts = BTreeMap::new();
113        for addr in addresses {
114            let pre = trace.pre.get(addr);
115            let post = trace.post.get(addr);
116            let diff = match (pre, post) {
117                (Some(pre), None) => destroyed(pre),
118                (None, Some(post)) => modified(AccountStatus::Created, None, post),
119                (Some(pre), Some(post)) => modified(AccountStatus::Modified, Some(pre), post),
120                (None, None) => unreachable!("address comes from pre or post"),
121            };
122            if diff.has_changes() {
123                accounts.insert(*addr, diff);
124            }
125        }
126        Self { accounts }
127    }
128
129    /// Folds per-transaction diffs, applied in order, into one net diff.
130    pub fn compose<'a>(diffs: impl IntoIterator<Item = &'a StateDiff>) -> Self {
131        let mut out = StateDiff::default();
132        for diff in diffs {
133            out = out.then(diff);
134        }
135        out
136    }
137
138    /// The net effect of `self` followed by `next`.
139    pub fn then(&self, next: &StateDiff) -> StateDiff {
140        let mut accounts = self.accounts.clone();
141        for (addr, later) in &next.accounts {
142            match accounts.remove(addr) {
143                None => {
144                    accounts.insert(*addr, later.clone());
145                }
146                Some(earlier) => {
147                    let merged = merge(earlier, later);
148                    if merged.has_changes() {
149                        accounts.insert(*addr, merged);
150                    }
151                }
152            }
153        }
154        StateDiff { accounts }
155    }
156
157    pub fn is_empty(&self) -> bool {
158        self.accounts.is_empty()
159    }
160
161    pub fn summary(&self) -> Summary {
162        let mut s = Summary { accounts: self.accounts.len(), ..Summary::default() };
163        for acct in self.accounts.values() {
164            match acct.status {
165                AccountStatus::Created => s.created += 1,
166                AccountStatus::Destroyed => s.destroyed += 1,
167                AccountStatus::Modified => {}
168            }
169            if let Some(delta) = acct.balance_delta() {
170                s.balance_changes += 1;
171                let total = if delta.negative { &mut s.total_debited } else { &mut s.total_credited };
172                *total = total.checked_add(delta.magnitude).unwrap_or(U256::MAX);
173            }
174            s.nonce_changes += usize::from(acct.nonce.is_some());
175            s.code_changes += usize::from(acct.code.is_some() || acct.code_hash.is_some());
176            s.storage_changes += acct.storage.len();
177        }
178        s
179    }
180}
181
182fn destroyed(pre: &AccountState) -> AccountDiff {
183    let mut d = AccountDiff::empty(AccountStatus::Destroyed);
184    d.balance = Change::new(balance_of(Some(pre)), U256::ZERO);
185    d.nonce = Change::new(nonce_of(Some(pre)), 0);
186    d.code = pre.code.clone().and_then(|c| Change::new(c, Bytes::default()));
187    d.code_hash = pre.code_hash.and_then(|h| Change::new(h, EMPTY_CODE_HASH));
188    for (slot, value) in pre.storage.iter().flatten() {
189        if let Some(c) = Change::new(*value, B256::ZERO) {
190            d.storage.insert(*slot, c);
191        }
192    }
193    d
194}
195
196fn modified(status: AccountStatus, pre: Option<&AccountState>, post: &AccountState) -> AccountDiff {
197    let mut d = AccountDiff::empty(status);
198    d.balance = post.balance.and_then(|to| Change::new(balance_of(pre), to));
199    d.nonce = post.nonce.and_then(|to| Change::new(nonce_of(pre), to));
200    d.code = post.code.clone().and_then(|to| Change::new(code_of(pre), to));
201    d.code_hash = post.code_hash.and_then(|to| Change::new(code_hash_of(pre), to));
202
203    let empty = BTreeMap::new();
204    let pre_slots = pre.and_then(|p| p.storage.as_ref()).unwrap_or(&empty);
205    let post_slots = post.storage.as_ref().unwrap_or(&empty);
206    for slot in pre_slots.keys().chain(post_slots.keys()) {
207        let from = pre_slots.get(slot).copied().unwrap_or(B256::ZERO);
208        let to = post_slots.get(slot).copied().unwrap_or(B256::ZERO);
209        if let Some(c) = Change::new(from, to) {
210            d.storage.insert(*slot, c);
211        }
212    }
213    d
214}
215
216fn merge_field<T: Clone + PartialEq>(a: &Option<Change<T>>, b: &Option<Change<T>>) -> Option<Change<T>> {
217    match (a, b) {
218        (None, None) => None,
219        (Some(x), None) => Some(x.clone()),
220        (None, Some(y)) => Some(y.clone()),
221        (Some(x), Some(y)) => Change::new(x.from.clone(), y.to.clone()),
222    }
223}
224
225fn merge(earlier: AccountDiff, later: &AccountDiff) -> AccountDiff {
226    use AccountStatus::*;
227    let status = match (earlier.status, later.status) {
228        (Created, Destroyed) => Created,
229        (_, Destroyed) => Destroyed,
230        (Created, _) => Created,
231        (Destroyed, _) => Modified,
232        (Modified, s) => s,
233    };
234    let mut storage = earlier.storage;
235    for (slot, change) in &later.storage {
236        match storage.remove(slot) {
237            None => {
238                storage.insert(*slot, *change);
239            }
240            Some(first) => {
241                if let Some(c) = Change::new(first.from, change.to) {
242                    storage.insert(*slot, c);
243                }
244            }
245        }
246    }
247    AccountDiff {
248        status,
249        balance: merge_field(&earlier.balance, &later.balance),
250        nonce: merge_field(&earlier.nonce, &later.nonce),
251        code: merge_field(&earlier.code, &later.code),
252        code_hash: merge_field(&earlier.code_hash, &later.code_hash),
253        storage,
254    }
255}
256
257/// A transaction's diff, labelled with its hash when known.
258#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
259pub struct TxDiff {
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub tx_hash: Option<B256>,
262    pub diff: StateDiff,
263}
264
265pub fn diffs_from_traces(traces: &[TxTrace]) -> Vec<TxDiff> {
266    traces.iter().map(|t| TxDiff { tx_hash: t.tx_hash, diff: StateDiff::from_prestate(&t.diff) }).collect()
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::trace::parse_traces_str;
273
274    fn addr(n: u8) -> Address {
275        Address([n; 20])
276    }
277
278    fn word(n: u64) -> B256 {
279        let mut w = [0u8; 32];
280        w[24..].copy_from_slice(&n.to_be_bytes());
281        B256(w)
282    }
283
284    fn diff(json: &str) -> StateDiff {
285        StateDiff::from_prestate(&parse_traces_str(json).unwrap()[0].diff)
286    }
287
288    const A: &str = "0x0101010101010101010101010101010101010101";
289
290    #[test]
291    fn transfer_to_new_account() {
292        let d = diff(&format!(
293            r#"{{"pre":{{"{A}":{{"balance":"0x64","nonce":3}}}},
294                "post":{{"{A}":{{"balance":"0x50","nonce":4}},"0x0202020202020202020202020202020202020202":{{"balance":"0xa"}}}}}}"#
295        ));
296        let sender = &d.accounts[&addr(1)];
297        assert_eq!(sender.status, AccountStatus::Modified);
298        assert_eq!(sender.balance, Some(Change { from: U256::from_u128(100), to: U256::from_u128(80) }));
299        assert_eq!(sender.nonce, Some(Change { from: 3, to: 4 }));
300        let receiver = &d.accounts[&addr(2)];
301        assert_eq!(receiver.status, AccountStatus::Created);
302        assert_eq!(receiver.balance, Some(Change { from: U256::ZERO, to: U256::from_u128(10) }));
303        let s = d.summary();
304        assert_eq!((s.accounts, s.created, s.balance_changes), (2, 1, 2));
305        assert_eq!(s.total_credited, U256::from_u128(10));
306        assert_eq!(s.total_debited, U256::from_u128(20));
307    }
308
309    #[test]
310    fn storage_set_cleared_and_unchanged_fields() {
311        let d = diff(&format!(
312            r#"{{"pre":{{"{A}":{{"balance":"0x1","storage":{{"0x1":"0x7","0x2":"0x9"}}}}}},
313                "post":{{"{A}":{{"storage":{{"0x2":"0xa","0x3":"0x5"}}}}}}}}"#
314        ));
315        let a = &d.accounts[&addr(1)];
316        assert_eq!(a.balance, None, "balance missing from post is unchanged");
317        assert_eq!(a.storage[&word(1)], Change { from: word(7), to: word(0) }, "cleared slot");
318        assert_eq!(a.storage[&word(2)], Change { from: word(9), to: word(10) });
319        assert_eq!(a.storage[&word(3)], Change { from: word(0), to: word(5) }, "new slot");
320    }
321
322    #[test]
323    fn contract_creation_and_destruction() {
324        let created = diff(&format!(
325            r#"{{"pre":{{}},"post":{{"{A}":{{"nonce":1,"code":"0x6000","storage":{{"0x0":"0x1"}}}}}}}}"#
326        ));
327        let c = &created.accounts[&addr(1)];
328        assert_eq!(c.status, AccountStatus::Created);
329        assert_eq!(c.code.as_ref().unwrap().to.0, vec![0x60, 0x00]);
330        assert_eq!(c.nonce, Some(Change { from: 0, to: 1 }));
331
332        let destroyed = diff(&format!(
333            r#"{{"pre":{{"{A}":{{"balance":"0x5","nonce":1,"code":"0x6000","storage":{{"0x0":"0x1","0x1":"0x0"}}}}}},"post":{{}}}}"#
334        ));
335        let d = &destroyed.accounts[&addr(1)];
336        assert_eq!(d.status, AccountStatus::Destroyed);
337        assert_eq!(d.balance.unwrap().to, U256::ZERO);
338        assert_eq!(d.code.as_ref().unwrap().to, Bytes::default());
339        assert_eq!(d.storage.len(), 1, "zero-valued slots are not changes");
340    }
341
342    #[test]
343    fn composition_nets_out_changes() {
344        let first = diff(&format!(
345            r#"{{"pre":{{"{A}":{{"balance":"0x10","storage":{{"0x1":"0x1"}}}}}},"post":{{"{A}":{{"balance":"0x20","storage":{{"0x1":"0x2"}}}}}}}}"#
346        ));
347        let second = diff(&format!(
348            r#"{{"pre":{{"{A}":{{"balance":"0x20","storage":{{"0x1":"0x2"}}}}}},"post":{{"{A}":{{"balance":"0x10","storage":{{"0x1":"0x3"}}}}}}}}"#
349        ));
350        let net = StateDiff::compose([&first, &second]);
351        let a = &net.accounts[&addr(1)];
352        assert_eq!(a.balance, None, "balance returned to its starting value");
353        assert_eq!(a.storage[&word(1)], Change { from: word(1), to: word(3) });
354
355        let back = diff(&format!(
356            r#"{{"pre":{{"{A}":{{"balance":"0x10","storage":{{"0x1":"0x3"}}}}}},"post":{{"{A}":{{"storage":{{"0x1":"0x1"}}}}}}}}"#
357        ));
358        assert!(StateDiff::compose([&first, &second, &back]).is_empty());
359    }
360
361    #[test]
362    fn created_then_destroyed_vanishes() {
363        let created = diff(&format!(r#"{{"pre":{{}},"post":{{"{A}":{{"balance":"0x5"}}}}}}"#));
364        let destroyed = diff(&format!(r#"{{"pre":{{"{A}":{{"balance":"0x5"}}}},"post":{{}}}}"#));
365        assert!(StateDiff::compose([&created, &destroyed]).is_empty());
366        let recreated = StateDiff::compose([&destroyed, &created]);
367        assert!(recreated.is_empty(), "destroyed then recreated with the same balance is no net change");
368    }
369}