aion_verify/lib.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! AION OS — first-party proof engine (`aion_verify`).
6//!
7//! It checks a predicate against **every** input in a bounded domain and returns a [`Verdict`] of either
8//! `Proven { cases }` (complete coverage — a proof) or `Refuted` **with the counterexample** that broke
9//! it. Over that domain the guarantee is the real thing: not a sample, the whole space.
10//!
11//! # Automatic properties — not just the predicate you write
12//!
13//! A bounded model checker like Kani verifies properties you never stated: index-out-of-bounds,
14//! arithmetic overflow, `unwrap` on `None`, division by zero. Those follow from the language, not from
15//! anything the author asserted. Two modules cover that ground here, by different routes:
16//!
17//! - [`safety::verify_no_panic`] runs the code over every input in a bounded domain and catches any
18//! unwind. Rust already emits those checks as panics, so exhaustive execution proves no input in the
19//! domain can panic. Requires the `std` feature and an unwinding profile.
20//! - [`symbolic::prove_no_overflow`] answers the same question *symbolically*, over unbounded domains
21//! and independent of the build profile — which matters because Rust only panics on integer overflow
22//! under `debug-assertions`, and wraps silently in release.
23//!
24//! # What this still is not
25//!
26//! Two real gaps remain against a compiler-driven checker, and they are worth stating plainly:
27//!
28//! - **It does not read your code.** Kani compiles actual Rust MIR and reasons about the program as
29//! written. Tier 4 here executes a closure you hand it; tier 5 analyses an [`symbolic::Expr`] you
30//! build by hand. Modelling a function as an `Expr` is manual work, and a model that drifts from the
31//! implementation proves things about the model, not the code.
32//! - **It only covers code that is reached.** Tier 4 covers exactly the paths the enumerated inputs
33//! take, and cannot see a function nobody called. Kani, being compiler-driven, has no such limit.
34//!
35//! Also inherent to the approach: concrete enumeration cannot span astronomically large domains (tier 5
36//! exists for that, at the cost of `Unknown` answers where the interval abstraction loses precision),
37//! and a passing verdict can still be hollow — see [`Verdict::is_vacuous`].
38//!
39//! So `aion_verify` is the everyday, zero-dependency engine for bounded invariants and automatic
40//! arithmetic safety, and **Kani remains the independent, third-party formal check**. It complements
41//! Kani; it does not replace it.
42//!
43//! `no_std`, `#![forbid(unsafe_code)]`.
44#![forbid(unsafe_code)]
45#![cfg_attr(not(feature = "std"), no_std)]
46
47extern crate alloc;
48
49/// Automatic safety checking — panics (index-out-of-bounds, overflow, `unwrap`, division by zero)
50/// found without writing a predicate, the way a bounded model checker does. Requires the `std`
51/// feature. See [`safety`].
52#[cfg(feature = "std")]
53pub mod safety;
54
55/// TIER 5 — symbolic verification over unbounded domains (interval abstract interpretation). Proves
56/// properties over *all* of `u64` without enumerating it, entirely in first-party Rust. See [`symbolic`].
57pub mod symbolic;
58
59/// A tamper-evident, append-only hash-chain [`ledger`] (with a pure-Rust SHA-512) for recording proof
60/// results so they cannot be forged or silently deleted.
61pub mod ledger;
62
63/// Post-quantum authenticity: a hash-based (WOTS) [`pqsig`] signature over the ledger head, so the log
64/// is provably yours and immune to Shor's algorithm — using only SHA-512, still zero-dependency.
65pub mod pqsig;
66
67/// Many-time post-quantum signatures: a Merkle tree ([`mss`], XMSS-style) over the one-time WOTS, so
68/// one published root signs `2^height` proofs from a single key.
69pub mod mss;
70
71/// The outcome of a proof attempt over a domain.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub enum Verdict<T> {
74 /// The predicate held for every one of `cases` inputs — a proof over the domain.
75 Proven { cases: u64 },
76 /// The predicate failed; `counterexample` is the input that broke it, after `checked` passing inputs.
77 Refuted { counterexample: T, checked: u64 },
78}
79
80impl<T> Verdict<T> {
81 /// True when the predicate was never refuted.
82 ///
83 /// **Caution — this is true for a vacuous proof.** A `Proven { cases: 0 }` verdict means the
84 /// predicate was never actually evaluated, because the domain was empty or a precondition filtered
85 /// every input out. Prefer [`is_proven_nonvacuous`](Self::is_proven_nonvacuous) in assertions.
86 pub fn is_proven(&self) -> bool {
87 matches!(self, Verdict::Proven { .. })
88 }
89
90 /// True when the verdict is `Proven` but **zero inputs were checked** — a vacuous proof.
91 ///
92 /// This is the classic vacuity problem from model checking. `for_all_where(inputs, precond, pred)`
93 /// reports `Proven` when `precond` rejected every input: nothing was tested, so nothing was proven,
94 /// yet the verdict reads as success. The usual cause is a precondition that over-constrains — an
95 /// `assume` that is stricter than the author believed, or that contradicts the domain outright.
96 ///
97 /// ```
98 /// use aion_verify::for_all_where;
99 /// // No u8 satisfies both `x > 200` and `x < 100`, so nothing is ever checked.
100 /// // `black_box` hides the contradiction from the optimizer and the linter.
101 /// let hi = core::hint::black_box(100u8);
102 /// let v = for_all_where(0u8..=255, |&x| x > 200 && x < hi, |_| false);
103 /// assert!(v.is_proven(), "reads as success...");
104 /// assert!(v.is_vacuous(), "...but proves nothing -- note the predicate is literally `false`");
105 /// assert!(!v.is_proven_nonvacuous());
106 /// ```
107 pub fn is_vacuous(&self) -> bool {
108 matches!(self, Verdict::Proven { cases: 0 })
109 }
110
111 /// [`is_proven`](Self::is_proven) with the vacuity hole closed: the predicate held **and** at least
112 /// one input actually reached it. This is what a test assertion should use.
113 ///
114 /// ```
115 /// use aion_verify::for_all_in;
116 /// let v = for_all_in(0, 100, |x| x <= 100);
117 /// assert!(v.is_proven_nonvacuous());
118 /// ```
119 ///
120 /// # What this does not catch
121 ///
122 /// Vacuity comes in two forms, and only one is detectable from outside the predicate:
123 ///
124 /// - **Empty domain** — nothing was checked. Decidable; this method catches it.
125 /// - **Trivial predicate** — plenty was checked, but the predicate cannot fail. `|x: i8| x <= 127`
126 /// is true by the type's own range, so it proves nothing about the code under test. No amount of
127 /// enumeration distinguishes a trivially-true property from a hard-won one, so this is *not*
128 /// detectable here. Guard against it by writing predicates that compare against an independently
129 /// computed reference value, or by confirming the proof fails when the implementation is mutated.
130 pub fn is_proven_nonvacuous(&self) -> bool {
131 matches!(self, Verdict::Proven { cases } if *cases > 0)
132 }
133 /// Inputs examined (all of them on Proven; the ones that passed before the failure on Refuted).
134 pub fn cases(&self) -> u64 {
135 match self {
136 Verdict::Proven { cases } => *cases,
137 Verdict::Refuted { checked, .. } => *checked,
138 }
139 }
140 pub fn counterexample(&self) -> Option<&T> {
141 match self {
142 Verdict::Refuted { counterexample, .. } => Some(counterexample),
143 Verdict::Proven { .. } => None,
144 }
145 }
146}
147
148/// Prove `pred` holds for EVERY item produced by `inputs`. Complete coverage of the domain = a proof.
149pub fn for_all<I, T, F>(inputs: I, pred: F) -> Verdict<T>
150where
151 I: IntoIterator<Item = T>,
152 F: Fn(&T) -> bool,
153{
154 let mut n = 0u64;
155 for x in inputs {
156 if !pred(&x) {
157 return Verdict::Refuted {
158 counterexample: x,
159 checked: n,
160 };
161 }
162 n += 1;
163 }
164 Verdict::Proven { cases: n }
165}
166
167/// Like [`for_all`] but only over inputs satisfying `precond` — the equivalent of a `kani::assume` guard.
168/// Proves `pred` on every input where the precondition holds.
169///
170/// # Vacuity warning
171///
172/// If `precond` rejects every input, this returns `Proven { cases: 0 }` — a **vacuous** proof that
173/// reads as success while having tested nothing. This is the most common way a proof suite silently
174/// stops proving anything: a precondition drifts out of sync with the domain, and every test still
175/// passes. Assert with [`Verdict::is_proven_nonvacuous`] rather than [`Verdict::is_proven`], or check
176/// [`Verdict::cases`] against the count you expect.
177pub fn for_all_where<I, T, P, F>(inputs: I, precond: P, pred: F) -> Verdict<T>
178where
179 I: IntoIterator<Item = T>,
180 P: Fn(&T) -> bool,
181 F: Fn(&T) -> bool,
182{
183 let mut n = 0u64;
184 for x in inputs {
185 if !precond(&x) {
186 continue;
187 }
188 if !pred(&x) {
189 return Verdict::Refuted {
190 counterexample: x,
191 checked: n,
192 };
193 }
194 n += 1;
195 }
196 Verdict::Proven { cases: n }
197}
198
199/// Exhaustive proof over the entire `u8` domain (all 256 values).
200pub fn for_all_u8<F: Fn(u8) -> bool>(pred: F) -> Verdict<u8> {
201 for_all(0u8..=255, |&x| pred(x))
202}
203
204/// Exhaustive proof over the inclusive range `[lo, hi]`.
205pub fn for_all_in<F: Fn(u64) -> bool>(lo: u64, hi: u64, pred: F) -> Verdict<u64> {
206 for_all(lo..=hi, |&x| pred(x))
207}
208
209/// Exhaustive proof over the cartesian product of two finite domains (binary invariants). Returns the
210/// `(a, b)` pair that breaks `pred`, if any.
211pub fn for_all_pairs<A: Clone, B: Clone, F: Fn(&A, &B) -> bool>(
212 a: &[A],
213 b: &[B],
214 pred: F,
215) -> Verdict<(A, B)> {
216 let mut n = 0u64;
217 for x in a {
218 for y in b {
219 if !pred(x, y) {
220 return Verdict::Refuted {
221 counterexample: (x.clone(), y.clone()),
222 checked: n,
223 };
224 }
225 n += 1;
226 }
227 }
228 Verdict::Proven { cases: n }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn for_all_proves_a_true_predicate_over_the_whole_domain() {
237 let v = for_all_u8(|x| (x as u16) + 1 > x as u16);
238 assert!(v.is_proven());
239 assert_eq!(v.cases(), 256, "every u8 checked — a proof, not a sample");
240 }
241
242 #[test]
243 fn vacuous_proofs_are_flagged_when_a_precondition_filters_everything_out() {
244 // The predicate is `false` -- it could never hold for any input. But the precondition is
245 // unsatisfiable, so the predicate is never reached and the verdict reads as Proven.
246 // `black_box` keeps the contradiction opaque to the optimizer and to clippy, which would
247 // otherwise reject `x > 200 && x < 100` as a comparison that can never be true -- correct in
248 // general, and exactly the situation being tested here.
249 let hi = core::hint::black_box(100u8);
250 let v = for_all_where(0u8..=255, |&x| x > 200 && x < hi, |_| false);
251
252 assert!(v.is_proven(), "the legacy check reports success");
253 assert_eq!(v.cases(), 0, "because nothing was ever checked");
254 assert!(
255 v.is_vacuous(),
256 "which is exactly what is_vacuous exists to surface"
257 );
258 assert!(
259 !v.is_proven_nonvacuous(),
260 "and the safe assertion refuses it"
261 );
262 }
263
264 #[test]
265 fn an_empty_domain_is_vacuous_too() {
266 // Not just preconditions: any empty input iterator yields the same hollow Proven.
267 let v: Verdict<u64> = for_all_in(10, 0, |_| false);
268 assert!(v.is_vacuous(), "lo > hi means an empty range");
269 assert!(!v.is_proven_nonvacuous());
270 }
271
272 #[test]
273 fn a_real_proof_is_nonvacuous() {
274 let v = for_all_u8(|x| (x as u16) + 1 > x as u16);
275 assert!(
276 v.is_proven_nonvacuous(),
277 "256 inputs actually reached the predicate"
278 );
279 assert!(!v.is_vacuous());
280
281 // A satisfiable precondition still leaves witnesses behind.
282 let w = for_all_where(0u8..=255, |&x| x % 2 == 0, |&x| x % 2 == 0);
283 assert!(w.is_proven_nonvacuous());
284 assert_eq!(w.cases(), 128);
285 }
286
287 #[test]
288 fn refuted_verdicts_are_never_vacuous() {
289 // A counterexample is proof the predicate was reached, so vacuity cannot apply.
290 let v = for_all_u8(|x| x < 200);
291 assert!(!v.is_vacuous());
292 assert!(
293 !v.is_proven_nonvacuous(),
294 "refuted is not proven, vacuous or otherwise"
295 );
296 }
297
298 #[test]
299 fn for_all_refutes_and_returns_the_counterexample() {
300 let v = for_all_u8(|x| x < 200);
301 assert!(!v.is_proven());
302 assert_eq!(v.counterexample(), Some(&200u8));
303 assert_eq!(
304 v.cases(),
305 200,
306 "200 values passed before the counterexample"
307 );
308 }
309
310 #[test]
311 fn for_all_where_applies_a_precondition() {
312 let v = for_all_where(0u16..=255, |x| x % 2 == 0, |x| (x * 2) % 2 == 0);
313 assert!(v.is_proven());
314 assert_eq!(v.cases(), 128, "only the 128 even values were in scope");
315 }
316
317 #[test]
318 fn for_all_in_covers_a_bounded_range() {
319 let v = for_all_in(10, 20, |x| (10..=20).contains(&x));
320 assert!(v.is_proven());
321 assert_eq!(v.cases(), 11);
322 }
323
324 #[test]
325 fn for_all_pairs_covers_the_product_and_finds_a_bad_pair() {
326 let a = [1u32, 2, 3];
327 let b = [10u32, 20];
328 let good = for_all_pairs(&a, &b, |&x, &y| x + y == y + x);
329 assert!(good.is_proven());
330 assert_eq!(good.cases(), 6, "3 x 2 pairs all checked");
331 let bad = for_all_pairs(&a, &b, |&x, &y| x + y != 12);
332 assert_eq!(
333 bad.counterexample(),
334 Some(&(2u32, 10u32)),
335 "2+10==12 breaks it"
336 );
337 }
338}
339
340// ── TIER 5 — third-party formal confirmation (Kani) ──────────────────────────────────────────────────
341#[cfg(kani)]
342mod kani_proofs {
343 use super::*;
344
345 /// Independent confirmation: for any bound b <= 16, for_all_in(0, b, |x| x <= b) is Proven and checks
346 /// exactly b+1 cases. Kani proves it SYMBOLICALLY — every b in 0..=16 over all execution paths at once,
347 /// confirming our enumerating engine's own claim about itself. Bounded to 16 (with a matching unwind so
348 /// CBMC fully unwinds the enumeration loop and terminates); the tier-4 test already exercises the full
349 /// 0..=255 range concretely, so this adds symbolic assurance on top rather than replacing it.
350 #[kani::proof]
351 #[kani::unwind(18)]
352 fn for_all_in_is_sound() {
353 let b: u64 = kani::any();
354 kani::assume(b <= 16);
355 let v = for_all_in(0, b, |x| x <= b);
356 assert!(v.is_proven());
357 assert!(v.cases() == b + 1);
358 }
359}