Skip to main content

evm_lens_core/storage/
diff.rs

1use serde::{Deserialize, Serialize};
2
3use super::layout::{Provenance, StorageEntry, StorageLayout, StorageType};
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
6pub enum DiffStatus {
7    Same,
8    Added,
9    Removed,
10    TypeChanged,
11    PackingChanged,
12}
13
14#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
15pub enum SeverityGrade {
16    Ok,
17    Risk,
18    Break,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct DiffEntry {
23    pub slot: u128,
24    pub old: Option<StorageEntry>,
25    pub new: Option<StorageEntry>,
26    pub status: DiffStatus,
27    pub grade: SeverityGrade,
28    pub provenance_old: Option<Provenance>,
29    pub provenance_new: Option<Provenance>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33pub struct Summary {
34    pub added: usize,
35    pub removed: usize,
36    pub type_changed: usize,
37    pub packing_changed: usize,
38    pub same: usize,
39    pub max_grade: SeverityGrade,
40}
41
42fn grade_for(status: &DiffStatus) -> SeverityGrade {
43    match status {
44        DiffStatus::Same => SeverityGrade::Ok,
45        DiffStatus::Added => SeverityGrade::Ok,
46        DiffStatus::Removed => SeverityGrade::Break,
47        DiffStatus::TypeChanged => SeverityGrade::Break,
48        DiffStatus::PackingChanged => SeverityGrade::Risk,
49    }
50}
51
52fn is_same_type(a: &StorageType, b: &StorageType) -> bool {
53    a == b
54}
55
56fn packing_key(e: &StorageEntry) -> (Option<u8>, Option<u8>) {
57    (e.offset, e.size)
58}
59
60pub fn diff_layouts(old: &StorageLayout, new: &StorageLayout) -> (Vec<DiffEntry>, Summary) {
61    // Group by slot, keeping a single representative per slot for conservative policy.
62    // If multiple packed fields exist, we compare by type and packing attributes; if any differ, mark PackingChanged.
63
64    use std::collections::BTreeMap;
65
66    let mut old_map: BTreeMap<u128, Vec<&StorageEntry>> = BTreeMap::new();
67    for e in &old.entries {
68        old_map.entry(e.slot).or_default().push(e);
69    }
70
71    let mut new_map: BTreeMap<u128, Vec<&StorageEntry>> = BTreeMap::new();
72    for e in &new.entries {
73        new_map.entry(e.slot).or_default().push(e);
74    }
75
76    let mut all_slots: BTreeMap<u128, ()> = BTreeMap::new();
77    for s in old_map.keys() {
78        all_slots.insert(*s, ());
79    }
80    for s in new_map.keys() {
81        all_slots.insert(*s, ());
82    }
83
84    let mut diffs: Vec<DiffEntry> = Vec::new();
85    let mut summary = Summary {
86        added: 0,
87        removed: 0,
88        type_changed: 0,
89        packing_changed: 0,
90        same: 0,
91        max_grade: SeverityGrade::Ok,
92    };
93
94    for slot in all_slots.keys().copied() {
95        let old_entries = old_map.get(&slot).cloned().unwrap_or_default();
96        let new_entries = new_map.get(&slot).cloned().unwrap_or_default();
97
98        let status = match (old_entries.is_empty(), new_entries.is_empty()) {
99            (true, false) => DiffStatus::Added,
100            (false, true) => DiffStatus::Removed,
101            (true, true) => continue,
102            (false, false) => {
103                // Compare conservatively
104                // If any type differs across matched pairs, TypeChanged.
105                // Else if packing differs (offset/size counts differ), PackingChanged.
106                let mut type_changed = false;
107                let mut packing_changed = false;
108
109                let min_len = old_entries.len().min(new_entries.len());
110                for i in 0..min_len {
111                    if !is_same_type(&old_entries[i].r#type, &new_entries[i].r#type) {
112                        type_changed = true;
113                        break;
114                    }
115                    if packing_key(old_entries[i]) != packing_key(new_entries[i]) {
116                        packing_changed = true;
117                    }
118                }
119                if !type_changed {
120                    if old_entries.len() != new_entries.len() {
121                        packing_changed = true;
122                    }
123                }
124
125                if type_changed {
126                    DiffStatus::TypeChanged
127                } else if packing_changed {
128                    DiffStatus::PackingChanged
129                } else {
130                    DiffStatus::Same
131                }
132            }
133        };
134
135        let grade = grade_for(&status);
136        summary.max_grade = summary.max_grade.max(grade);
137        match status {
138            DiffStatus::Same => summary.same += 1,
139            DiffStatus::Added => summary.added += 1,
140            DiffStatus::Removed => summary.removed += 1,
141            DiffStatus::TypeChanged => summary.type_changed += 1,
142            DiffStatus::PackingChanged => summary.packing_changed += 1,
143        }
144
145        let old_rep = old_entries.first().cloned().cloned();
146        let new_rep = new_entries.first().cloned().cloned();
147
148        diffs.push(DiffEntry {
149            slot,
150            old: old_rep.clone(),
151            new: new_rep.clone(),
152            status,
153            grade,
154            provenance_old: old_rep.as_ref().map(|e| e.provenance),
155            provenance_new: new_rep.as_ref().map(|e| e.provenance),
156        });
157    }
158
159    (diffs, summary)
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    fn entry(
167        slot: u128,
168        ty: StorageType,
169        offset: Option<u8>,
170        size: Option<u8>,
171        prov: Provenance,
172    ) -> StorageEntry {
173        StorageEntry {
174            slot,
175            offset,
176            size,
177            r#type: ty,
178            label: None,
179            provenance: prov,
180        }
181    }
182
183    #[test]
184    fn added_and_removed() {
185        let old = StorageLayout {
186            entries: vec![entry(
187                0,
188                StorageType::Unknown,
189                None,
190                None,
191                Provenance::HeuristicTrace,
192            )],
193        };
194        let new = StorageLayout {
195            entries: vec![entry(
196                1,
197                StorageType::Unknown,
198                None,
199                None,
200                Provenance::HeuristicTrace,
201            )],
202        };
203        let (_diffs, summary) = diff_layouts(&old, &new);
204        assert_eq!(summary.added, 1);
205        assert_eq!(summary.removed, 1);
206        assert_eq!(summary.max_grade, SeverityGrade::Break);
207    }
208
209    #[test]
210    fn type_changed_breaks() {
211        let old = StorageLayout {
212            entries: vec![entry(
213                0,
214                StorageType::Uint { bits: 256 },
215                None,
216                None,
217                Provenance::CompilerMetadata,
218            )],
219        };
220        let new = StorageLayout {
221            entries: vec![entry(
222                0,
223                StorageType::Address,
224                None,
225                None,
226                Provenance::CompilerMetadata,
227            )],
228        };
229        let (_diffs, summary) = diff_layouts(&old, &new);
230        assert_eq!(summary.type_changed, 1);
231        assert_eq!(summary.max_grade, SeverityGrade::Break);
232    }
233
234    #[test]
235    fn packing_changed_risk() {
236        let old = StorageLayout {
237            entries: vec![entry(
238                0,
239                StorageType::Uint { bits: 256 },
240                Some(0),
241                Some(32),
242                Provenance::CompilerMetadata,
243            )],
244        };
245        let new = StorageLayout {
246            entries: vec![entry(
247                0,
248                StorageType::Uint { bits: 256 },
249                Some(1),
250                Some(31),
251                Provenance::CompilerMetadata,
252            )],
253        };
254        let (_diffs, summary) = diff_layouts(&old, &new);
255        assert_eq!(summary.packing_changed, 1);
256        assert_eq!(summary.max_grade, SeverityGrade::Risk);
257    }
258
259    #[test]
260    fn same_ok() {
261        let e = entry(
262            2,
263            StorageType::Bool,
264            None,
265            None,
266            Provenance::CompilerMetadata,
267        );
268        let old = StorageLayout {
269            entries: vec![e.clone()],
270        };
271        let new = StorageLayout { entries: vec![e] };
272        let (_diffs, summary) = diff_layouts(&old, &new);
273        assert_eq!(summary.same, 1);
274        assert_eq!(summary.max_grade, SeverityGrade::Ok);
275    }
276}