Skip to main content

alloy_eip7928/
diff.rs

1//! Helpers for finding the first divergence between block access lists.
2
3use crate::AccountChanges;
4use alloy_primitives::Address;
5use core::{cmp::Ordering, fmt};
6
7/// Compact summary of the first difference between two block access lists.
8///
9/// The diff only reports the first account position that differs. This keeps diagnostics cheap for
10/// callers that only need enough context to explain why two BALs do not hash to the same value.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct BalDiff {
13    /// Number of account entries in the left-hand BAL.
14    pub left_accounts: usize,
15    /// Number of account entries in the right-hand BAL.
16    pub right_accounts: usize,
17    /// First account-level difference, if any.
18    pub first_diff: Option<AccountDiff>,
19}
20
21impl BalDiff {
22    /// Returns the first divergence between two block access lists.
23    pub fn between(left: &[AccountChanges], right: &[AccountChanges]) -> Self {
24        let mut index = 0;
25        let first_diff = loop {
26            match (left.get(index), right.get(index)) {
27                (Some(left_account), Some(right_account)) => {
28                    match left_account.address.cmp(&right_account.address) {
29                        Ordering::Less | Ordering::Greater => {
30                            break Some(AccountDiff::address_mismatch(
31                                index,
32                                left_account,
33                                right_account,
34                            ));
35                        }
36                        Ordering::Equal => {
37                            let fields_differ = AccountFieldDiff::new(left_account, right_account);
38                            if fields_differ.is_divergent() {
39                                break Some(AccountDiff {
40                                    index,
41                                    left: Some(AccountSummary::from_account(left_account)),
42                                    right: Some(AccountSummary::from_account(right_account)),
43                                    fields_differ,
44                                });
45                            }
46                            index += 1;
47                        }
48                    }
49                }
50                (Some(account), None) => break Some(AccountDiff::left_only(index, account)),
51                (None, Some(account)) => break Some(AccountDiff::right_only(index, account)),
52                (None, None) => break None,
53            }
54        };
55
56        Self { left_accounts: left.len(), right_accounts: right.len(), first_diff }
57    }
58
59    /// Returns `true` if the compared BALs do not diverge.
60    #[inline]
61    pub const fn is_empty(&self) -> bool {
62        self.first_diff.is_none()
63    }
64}
65
66impl fmt::Display for BalDiff {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match &self.first_diff {
69            Some(diff) => write!(
70                f,
71                "accounts left={}, right={}; {}",
72                self.left_accounts, self.right_accounts, diff
73            ),
74            None => write!(
75                f,
76                "no BAL divergence (accounts left={}, right={})",
77                self.left_accounts, self.right_accounts
78            ),
79        }
80    }
81}
82
83/// Account-level details for the first differing BAL entry.
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct AccountDiff {
86    /// Account index in the BAL where the first difference was found.
87    pub index: usize,
88    /// Summary of the left-hand account entry at [`Self::index`], if present.
89    pub left: Option<AccountSummary>,
90    /// Summary of the right-hand account entry at [`Self::index`], if present.
91    pub right: Option<AccountSummary>,
92    /// Per-field differences when both entries have the same address.
93    pub fields_differ: AccountFieldDiff,
94}
95
96impl AccountDiff {
97    fn left_only(index: usize, account: &AccountChanges) -> Self {
98        Self {
99            index,
100            left: Some(AccountSummary::from_account(account)),
101            right: None,
102            fields_differ: AccountFieldDiff::default(),
103        }
104    }
105
106    fn right_only(index: usize, account: &AccountChanges) -> Self {
107        Self {
108            index,
109            left: None,
110            right: Some(AccountSummary::from_account(account)),
111            fields_differ: AccountFieldDiff::default(),
112        }
113    }
114
115    fn address_mismatch(index: usize, left: &AccountChanges, right: &AccountChanges) -> Self {
116        Self {
117            index,
118            left: Some(AccountSummary::from_account(left)),
119            right: Some(AccountSummary::from_account(right)),
120            fields_differ: AccountFieldDiff::default(),
121        }
122    }
123}
124
125impl fmt::Display for AccountDiff {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        write!(f, "first difference at account index {}", self.index)?;
128        match (&self.left, &self.right) {
129            (Some(left), Some(right)) if left.address != right.address => {
130                write!(f, ": address mismatch, left={left}, right={right}")
131            }
132            (Some(left), Some(right)) => {
133                write!(f, ": fields [{}] differ, left={left}, right={right}", self.fields_differ)
134            }
135            (Some(left), None) => write!(f, ": account only in left BAL, left={left}"),
136            (None, Some(right)) => write!(f, ": account only in right BAL, right={right}"),
137            (None, None) => f.write_str(": missing account summaries"),
138        }
139    }
140}
141
142/// Compact account summary included in [`AccountDiff`].
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct AccountSummary {
145    /// Account address for this BAL entry.
146    pub address: Address,
147    /// Number of changed storage slots.
148    pub storage_changes: usize,
149    /// Number of read storage slots.
150    pub storage_reads: usize,
151    /// Number of balance changes.
152    pub balance_changes: usize,
153    /// Number of nonce changes.
154    pub nonce_changes: usize,
155    /// Number of code changes.
156    pub code_changes: usize,
157}
158
159impl AccountSummary {
160    /// Creates a summary from a BAL account entry.
161    #[inline]
162    pub const fn from_account(account: &AccountChanges) -> Self {
163        Self {
164            address: account.address,
165            storage_changes: account.storage_changes.len(),
166            storage_reads: account.storage_reads.len(),
167            balance_changes: account.balance_changes.len(),
168            nonce_changes: account.nonce_changes.len(),
169            code_changes: account.code_changes.len(),
170        }
171    }
172}
173
174impl fmt::Display for AccountSummary {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        write!(
177            f,
178            "{} (storage_changes={}, storage_reads={}, balance_changes={}, nonce_changes={}, code_changes={})",
179            self.address,
180            self.storage_changes,
181            self.storage_reads,
182            self.balance_changes,
183            self.nonce_changes,
184            self.code_changes
185        )
186    }
187}
188
189/// Per-field difference flags for account entries with the same address.
190#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
191pub struct AccountFieldDiff {
192    /// `true` if `storage_changes` differ.
193    pub storage_changes: bool,
194    /// `true` if `storage_reads` differ.
195    pub storage_reads: bool,
196    /// `true` if `balance_changes` differ.
197    pub balance_changes: bool,
198    /// `true` if `nonce_changes` differ.
199    pub nonce_changes: bool,
200    /// `true` if `code_changes` differ.
201    pub code_changes: bool,
202}
203
204impl AccountFieldDiff {
205    /// Creates field-level diff flags for account entries with the same address.
206    pub fn new(left: &AccountChanges, right: &AccountChanges) -> Self {
207        Self {
208            storage_changes: left.storage_changes != right.storage_changes,
209            storage_reads: left.storage_reads != right.storage_reads,
210            balance_changes: left.balance_changes != right.balance_changes,
211            nonce_changes: left.nonce_changes != right.nonce_changes,
212            code_changes: left.code_changes != right.code_changes,
213        }
214    }
215
216    /// Returns `true` if any account field differs.
217    #[inline]
218    pub const fn is_divergent(&self) -> bool {
219        self.storage_changes
220            || self.storage_reads
221            || self.balance_changes
222            || self.nonce_changes
223            || self.code_changes
224    }
225
226    fn fmt_flag(
227        f: &mut fmt::Formatter<'_>,
228        wrote_field: &mut bool,
229        differs: bool,
230        name: &str,
231    ) -> fmt::Result {
232        if differs {
233            if *wrote_field {
234                f.write_str(", ")?;
235            }
236            f.write_str(name)?;
237            *wrote_field = true;
238        }
239        Ok(())
240    }
241}
242
243impl fmt::Display for AccountFieldDiff {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        let mut wrote_field = false;
246        Self::fmt_flag(f, &mut wrote_field, self.storage_changes, "storage_changes")?;
247        Self::fmt_flag(f, &mut wrote_field, self.storage_reads, "storage_reads")?;
248        Self::fmt_flag(f, &mut wrote_field, self.balance_changes, "balance_changes")?;
249        Self::fmt_flag(f, &mut wrote_field, self.nonce_changes, "nonce_changes")?;
250        Self::fmt_flag(f, &mut wrote_field, self.code_changes, "code_changes")?;
251        if !wrote_field {
252            f.write_str("none")?;
253        }
254        Ok(())
255    }
256}
257
258/// Returns a compact diff describing the first divergence between two BALs.
259pub fn first_bal_divergence(left: &[AccountChanges], right: &[AccountChanges]) -> BalDiff {
260    BalDiff::between(left, right)
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::{
267        BalanceChange, BlockAccessIndex, CodeChange, NonceChange, SlotChanges, StorageChange,
268        bal::Bal,
269    };
270    use alloc::format;
271    use alloy_primitives::{Address, Bytes, U256};
272
273    fn diagnostic_addr(byte: u8) -> Address {
274        let mut address = [0; 20];
275        address[19] = byte;
276        Address::from(address)
277    }
278
279    fn diagnostic_account(address: Address, balance: u64) -> AccountChanges {
280        AccountChanges {
281            address,
282            balance_changes: vec![BalanceChange::new(
283                BlockAccessIndex::new(1),
284                U256::from(balance),
285            )],
286            ..Default::default()
287        }
288    }
289
290    fn first_diff(left: &[AccountChanges], right: &[AccountChanges]) -> AccountDiff {
291        first_bal_divergence(left, right).first_diff.expect("expected BAL divergence")
292    }
293
294    fn assert_summary_address(summary: &Option<AccountSummary>, address: Address) {
295        assert_eq!(summary.as_ref().map(|summary| summary.address), Some(address));
296    }
297
298    #[test]
299    fn none_for_equal_bals() {
300        let account = diagnostic_account(diagnostic_addr(1), 1);
301        let right = vec![account.clone()];
302
303        let diff = first_bal_divergence(core::slice::from_ref(&account), &right);
304
305        assert_eq!(diff, BalDiff { left_accounts: 1, right_accounts: 1, first_diff: None });
306        assert!(diff.is_empty());
307    }
308
309    #[test]
310    fn reports_account_only_in_left() {
311        let left = vec![diagnostic_account(diagnostic_addr(1), 1)];
312
313        let diff = first_diff(&left, &[]);
314
315        assert_eq!(diff.index, 0);
316        assert_eq!(diff.left, Some(AccountSummary::from_account(&left[0])));
317        assert_eq!(diff.right, None);
318    }
319
320    #[test]
321    fn reports_tail_account_only_in_left_after_matching_prefix() {
322        let shared = diagnostic_account(diagnostic_addr(1), 1);
323        let left = vec![shared.clone(), diagnostic_account(diagnostic_addr(2), 2)];
324        let right = vec![shared];
325
326        let diff = first_diff(&left, &right);
327
328        assert_eq!(diff.index, 1);
329        assert_summary_address(&diff.left, diagnostic_addr(2));
330        assert_eq!(diff.right, None);
331    }
332
333    #[test]
334    fn reports_account_only_in_right() {
335        let right = vec![diagnostic_account(diagnostic_addr(1), 1)];
336
337        let diff = first_diff(&[], &right);
338
339        assert_eq!(diff.index, 0);
340        assert_eq!(diff.left, None);
341        assert_eq!(diff.right, Some(AccountSummary::from_account(&right[0])));
342    }
343
344    #[test]
345    fn reports_tail_account_only_in_right_after_matching_prefix() {
346        let shared = diagnostic_account(diagnostic_addr(1), 1);
347        let left = vec![shared.clone()];
348        let right = vec![shared, diagnostic_account(diagnostic_addr(2), 2)];
349
350        let diff = first_diff(&left, &right);
351
352        assert_eq!(diff.index, 1);
353        assert_eq!(diff.left, None);
354        assert_summary_address(&diff.right, diagnostic_addr(2));
355    }
356
357    #[test]
358    fn reports_changed_balance_field() {
359        let left = vec![diagnostic_account(diagnostic_addr(1), 1)];
360        let right = vec![diagnostic_account(diagnostic_addr(1), 2)];
361
362        let diff = first_diff(&left, &right);
363
364        assert_eq!(diff.index, 0);
365        assert_eq!(
366            diff.fields_differ,
367            AccountFieldDiff { balance_changes: true, ..Default::default() }
368        );
369        assert!(diff.left.is_some());
370        assert!(diff.right.is_some());
371        assert!(!first_bal_divergence(&left, &right).is_empty());
372    }
373
374    #[test]
375    fn reports_each_changed_account_field() {
376        let left = vec![AccountChanges {
377            address: diagnostic_addr(1),
378            storage_changes: vec![SlotChanges::new(
379                U256::from(1),
380                vec![StorageChange::new(BlockAccessIndex::new(1), U256::from(1))],
381            )],
382            storage_reads: vec![U256::from(2)],
383            balance_changes: vec![BalanceChange::new(BlockAccessIndex::new(3), U256::from(3))],
384            nonce_changes: vec![NonceChange::new(BlockAccessIndex::new(4), 4)],
385            code_changes: vec![CodeChange::new(BlockAccessIndex::new(5), Bytes::from_static(&[5]))],
386        }];
387        let right = vec![AccountChanges {
388            address: diagnostic_addr(1),
389            storage_changes: vec![SlotChanges::new(
390                U256::from(1),
391                vec![StorageChange::new(BlockAccessIndex::new(1), U256::from(10))],
392            )],
393            storage_reads: vec![U256::from(20)],
394            balance_changes: vec![BalanceChange::new(BlockAccessIndex::new(3), U256::from(30))],
395            nonce_changes: vec![NonceChange::new(BlockAccessIndex::new(4), 40)],
396            code_changes: vec![CodeChange::new(
397                BlockAccessIndex::new(5),
398                Bytes::from_static(&[50]),
399            )],
400        }];
401
402        let diff = first_diff(&left, &right);
403
404        assert_eq!(
405            diff.fields_differ,
406            AccountFieldDiff {
407                storage_changes: true,
408                storage_reads: true,
409                balance_changes: true,
410                nonce_changes: true,
411                code_changes: true,
412            }
413        );
414        assert_eq!(
415            diff.left,
416            Some(AccountSummary {
417                address: diagnostic_addr(1),
418                storage_changes: 1,
419                storage_reads: 1,
420                balance_changes: 1,
421                nonce_changes: 1,
422                code_changes: 1,
423            })
424        );
425    }
426
427    #[test]
428    fn reports_both_addresses_for_address_mismatch() {
429        let left = vec![
430            diagnostic_account(diagnostic_addr(1), 1),
431            diagnostic_account(diagnostic_addr(3), 1),
432        ];
433        let right = vec![
434            diagnostic_account(diagnostic_addr(2), 1),
435            diagnostic_account(diagnostic_addr(3), 2),
436        ];
437
438        let diff = first_diff(&left, &right);
439
440        assert_eq!(diff.index, 0);
441        assert_summary_address(&diff.left, diagnostic_addr(1));
442        assert_summary_address(&diff.right, diagnostic_addr(2));
443        assert_eq!(diff.fields_differ, AccountFieldDiff::default());
444    }
445
446    #[test]
447    fn stops_at_first_mismatch_after_matching_prefix() {
448        let shared = diagnostic_account(diagnostic_addr(1), 1);
449        let left = vec![
450            shared.clone(),
451            diagnostic_account(diagnostic_addr(2), 2),
452            diagnostic_account(diagnostic_addr(4), 4),
453        ];
454        let right = vec![
455            shared,
456            diagnostic_account(diagnostic_addr(3), 3),
457            diagnostic_account(diagnostic_addr(4), 5),
458        ];
459
460        let diff = first_diff(&left, &right);
461
462        assert_eq!(diff.index, 1);
463        assert_summary_address(&diff.left, diagnostic_addr(2));
464        assert_summary_address(&diff.right, diagnostic_addr(3));
465        assert_eq!(diff.fields_differ, AccountFieldDiff::default());
466    }
467
468    #[test]
469    fn bal_methods_compare_against_slices() {
470        let left = Bal::new(vec![diagnostic_account(diagnostic_addr(1), 1)]);
471        let right = vec![diagnostic_account(diagnostic_addr(1), 2)];
472
473        assert_eq!(left.diff(&right), BalDiff::between(left.as_slice(), &right));
474    }
475
476    #[test]
477    fn displays_equal_bals_without_diff() {
478        let account = diagnostic_account(diagnostic_addr(1), 1);
479        let right = vec![account.clone()];
480
481        assert_eq!(
482            format!("{}", first_bal_divergence(core::slice::from_ref(&account), &right)),
483            "no BAL divergence (accounts left=1, right=1)"
484        );
485    }
486
487    #[test]
488    fn displays_field_divergence() {
489        let left = vec![diagnostic_account(diagnostic_addr(1), 1)];
490        let right = vec![diagnostic_account(diagnostic_addr(1), 2)];
491
492        assert_eq!(
493            format!("{}", first_bal_divergence(&left, &right)),
494            concat!(
495                "accounts left=1, right=1; first difference at account index 0: ",
496                "fields [balance_changes] differ, ",
497                "left=0x0000000000000000000000000000000000000001 ",
498                "(storage_changes=0, storage_reads=0, balance_changes=1, nonce_changes=0, ",
499                "code_changes=0), ",
500                "right=0x0000000000000000000000000000000000000001 ",
501                "(storage_changes=0, storage_reads=0, balance_changes=1, nonce_changes=0, ",
502                "code_changes=0)"
503            )
504        );
505    }
506
507    #[test]
508    fn displays_address_mismatch() {
509        let left = vec![diagnostic_account(diagnostic_addr(1), 1)];
510        let right = vec![diagnostic_account(diagnostic_addr(2), 1)];
511
512        assert_eq!(
513            format!("{}", first_bal_divergence(&left, &right)),
514            concat!(
515                "accounts left=1, right=1; first difference at account index 0: ",
516                "address mismatch, ",
517                "left=0x0000000000000000000000000000000000000001 ",
518                "(storage_changes=0, storage_reads=0, balance_changes=1, nonce_changes=0, ",
519                "code_changes=0), ",
520                "right=0x0000000000000000000000000000000000000002 ",
521                "(storage_changes=0, storage_reads=0, balance_changes=1, nonce_changes=0, ",
522                "code_changes=0)"
523            )
524        );
525    }
526
527    #[test]
528    fn displays_missing_account_side() {
529        let left = vec![diagnostic_account(diagnostic_addr(1), 1)];
530
531        assert_eq!(
532            format!("{}", first_bal_divergence(&left, &[])),
533            concat!(
534                "accounts left=1, right=0; first difference at account index 0: ",
535                "account only in left BAL, ",
536                "left=0x0000000000000000000000000000000000000001 ",
537                "(storage_changes=0, storage_reads=0, balance_changes=1, nonce_changes=0, ",
538                "code_changes=0)"
539            )
540        );
541    }
542}