Skip to main content

hopper_core/invariant/
mod.rs

1//! Invariant engine -- post-execution correctness verification.
2//!
3//! Invariants are boolean conditions that must hold after any state mutation.
4//! They catch logic bugs, accounting errors, and constraint violations
5//! that slip past per-instruction validation.
6//!
7//! ## Usage
8//!
9//! ```ignore
10//! hopper_invariant! {
11//!     fn vault_solvent(vault: &Vault) -> bool {
12//!         vault.balance.get() <= vault_account.lamports()
13//!     }
14//! }
15//! ```
16//!
17//! ## Design
18//!
19//! - Invariants are **zero-cost in release builds** by default (cfg-gated)
20//! - Can be forced on with the `invariants` feature for auditing
21//! - `check_invariant` is always available for explicit checks
22//! - `InvariantSet` collects multiple invariants for batch checking
23
24use hopper_runtime::error::ProgramError;
25
26/// Check a single invariant condition.
27///
28/// Returns `Ok(())` if the condition holds, or `Err(ProgramError::Custom(code))`
29/// if violated. The error code identifies which invariant failed.
30#[inline(always)]
31pub fn check_invariant(condition: bool, invariant_code: u32) -> Result<(), ProgramError> {
32    if !condition {
33        return Err(ProgramError::Custom(invariant_code));
34    }
35    Ok(())
36}
37
38/// Check a single invariant with a closure (lazy evaluation).
39///
40/// The closure is only called when invariants are enabled.
41/// In release builds without the `invariants` feature, this is a no-op.
42#[inline(always)]
43pub fn check_invariant_fn<F: FnOnce() -> bool>(
44    f: F,
45    invariant_code: u32,
46) -> Result<(), ProgramError> {
47    if !f() {
48        return Err(ProgramError::Custom(invariant_code));
49    }
50    Ok(())
51}
52
53/// Batch invariant checker.
54///
55/// Collects multiple invariant results and reports the first failure.
56/// All invariants are checked even after a failure (useful for diagnostics).
57pub struct InvariantSet {
58    first_failure: Option<u32>,
59    checked: u16,
60    passed: u16,
61}
62
63impl InvariantSet {
64    /// Create a new empty invariant set.
65    #[inline(always)]
66    pub const fn new() -> Self {
67        Self {
68            first_failure: None,
69            checked: 0,
70            passed: 0,
71        }
72    }
73
74    /// Add an invariant check.
75    #[inline(always)]
76    pub fn check(&mut self, condition: bool, code: u32) {
77        self.checked += 1;
78        if condition {
79            self.passed += 1;
80        } else if self.first_failure.is_none() {
81            self.first_failure = Some(code);
82        }
83    }
84
85    /// Add an invariant check with lazy evaluation.
86    #[inline(always)]
87    pub fn check_fn<F: FnOnce() -> bool>(&mut self, f: F, code: u32) {
88        self.check(f(), code);
89    }
90
91    /// Get the number of invariants checked.
92    #[inline(always)]
93    pub fn checked_count(&self) -> u16 {
94        self.checked
95    }
96
97    /// Get the number of invariants that passed.
98    #[inline(always)]
99    pub fn passed_count(&self) -> u16 {
100        self.passed
101    }
102
103    /// Did all invariants pass?
104    #[inline(always)]
105    pub fn all_passed(&self) -> bool {
106        self.first_failure.is_none()
107    }
108
109    /// Finalize: return Ok if all passed, or the first failure code.
110    #[inline(always)]
111    pub fn finalize(self) -> Result<(), ProgramError> {
112        match self.first_failure {
113            None => Ok(()),
114            Some(code) => Err(ProgramError::Custom(code)),
115        }
116    }
117}
118
119impl Default for InvariantSet {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125/// Invariant descriptor for schema/tooling export.
126#[derive(Clone, Copy)]
127pub struct InvariantDescriptor {
128    /// Human-readable invariant name.
129    pub name: &'static str,
130    /// Error code if violated.
131    pub code: u32,
132    /// Description of what's being checked.
133    pub description: &'static str,
134}