Skip to main content

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}