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}