1use crate::AccountChanges;
4use alloy_primitives::Address;
5use core::{cmp::Ordering, fmt};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct BalDiff {
13 pub left_accounts: usize,
15 pub right_accounts: usize,
17 pub first_diff: Option<AccountDiff>,
19}
20
21impl BalDiff {
22 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 #[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#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct AccountDiff {
86 pub index: usize,
88 pub left: Option<AccountSummary>,
90 pub right: Option<AccountSummary>,
92 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct AccountSummary {
145 pub address: Address,
147 pub storage_changes: usize,
149 pub storage_reads: usize,
151 pub balance_changes: usize,
153 pub nonce_changes: usize,
155 pub code_changes: usize,
157}
158
159impl AccountSummary {
160 #[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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
191pub struct AccountFieldDiff {
192 pub storage_changes: bool,
194 pub storage_reads: bool,
196 pub balance_changes: bool,
198 pub nonce_changes: bool,
200 pub code_changes: bool,
202}
203
204impl AccountFieldDiff {
205 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 #[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
258pub 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}