From 50dab4cfb8b3d7b82834b54efb387f7f3011aaf4 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 12 May 2026 13:00:38 +0800 Subject: [PATCH] fix: add state consistency checks --- fuzz/fuzz_targets/fuzz_state_transition.rs | 20 ++++++++++ .../fuzz_validate_execute_consistency.rs | 38 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/fuzz/fuzz_targets/fuzz_state_transition.rs b/fuzz/fuzz_targets/fuzz_state_transition.rs index 1ba2a22..7cc184f 100644 --- a/fuzz/fuzz_targets/fuzz_state_transition.rs +++ b/fuzz/fuzz_targets/fuzz_state_transition.rs @@ -53,6 +53,26 @@ fuzz_target!(|data: &[u8]| { acc_id ); } + } else { + // INVARIANT: total balance of known accounts must be conserved on success. + // Catches double-credit and token-inflation bugs — two transfer paths that + // each credit the recipient without debiting the sender would inflate the + // total, but neither the rejection check nor any other per-account check + // catches it unless we compare the aggregate. + let total_before: u128 = init_accs + .iter() + .map(|&(acc_id, _)| state_snapshot.get_account_by_id(acc_id).balance) + .fold(0u128, u128::saturating_add); + let total_after: u128 = init_accs + .iter() + .map(|&(acc_id, _)| state.get_account_by_id(acc_id).balance) + .fold(0u128, u128::saturating_add); + assert_eq!( + total_before, + total_after, + "INVARIANT VIOLATION: total balance of genesis accounts changed after successful \ + transaction (possible double-credit or token-inflation bug)", + ); } } }); diff --git a/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs b/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs index e20158f..8710bf0 100644 --- a/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs +++ b/fuzz/fuzz_targets/fuzz_validate_execute_consistency.rs @@ -12,6 +12,12 @@ //! - every account changed by `execute_check_on_state` must appear in the diff; //! a silent state-widening bug (execute touches an extra account not declared //! in the diff) is caught by the reverse check. +//! +//! 3. **Balance conservation** — when both succeed, the sum of all known account +//! balances (genesis ∪ diff-declared) must be identical before and after the +//! transaction. This catches double-credit and token-inflation bugs that both +//! methods could agree on silently (INVARIANT 2a/2b only check consistency +//! between the two methods, not correctness of the arithmetic itself). use fuzz_props::arbitrary_types::ArbNSSATransaction; use libfuzzer_sys::fuzz_target; @@ -85,6 +91,38 @@ fuzz_target!(|wrapped: ArbNSSATransaction| { ); } } + + // INVARIANT 3 (balance conservation): Σ balances must be identical before and after + // a successful transaction over all known accounts (genesis ∪ diff-declared accounts). + // + // This catches double-credit and token-inflation bugs that both validate_on_state and + // execute_check_on_state could agree on silently — e.g. a transfer path that credits + // the recipient without debiting the sender. INVARIANT 2a/2b only check that the two + // methods agree with each other; they do not catch the case where both are wrong in the + // same direction. + // + // Limitation: accounts created brand-new by execute_check_on_state that are absent from + // both genesis and the diff are not included here (see the known limitation in INVARIANT + // 2b above). A transfer to a freshly-created account would inflate the known total. + let known_ids: std::collections::HashSet = init_accs + .iter() + .map(|&(id, _)| id) + .chain(public_diff.keys().copied()) + .collect(); + let total_before: u128 = known_ids + .iter() + .map(|id| state.get_account_by_id(*id).balance) + .fold(0u128, u128::saturating_add); + let total_after: u128 = known_ids + .iter() + .map(|id| exec_state.get_account_by_id(*id).balance) + .fold(0u128, u128::saturating_add); + assert_eq!( + total_before, + total_after, + "INVARIANT VIOLATION: total balance of known accounts changed after successful \ + transaction (possible double-credit or token-inflation bug)", + ); } (Err(_), Err(_)) => { // Both failed — correct.