Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions spark/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Summary of SPARK analysis
| `src/rust/integrity/solver_integrity.rs`
| Run #2 — proved (11/0)

| link:certificates/[certificates]
| `src/rust/verification/certificates.rs`
| Run #3 — proved (10/0)

|===

== Adding a new companion
Expand Down
1 change: 1 addition & 0 deletions spark/certificates/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.obj/
13 changes: 13 additions & 0 deletions spark/certificates/alire.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name = "certificates_spark"
description = "SPARK companion for echidna certificates module"
version = "0.1.0"
licenses = "PMPL-1.0-or-later"
maintainers = ["hyperpolymath@users.noreply.github.com"]
maintainers-logins = ["hyperpolymath"]

[build-switches]
"*".Ada_Version = "Ada2022"

[[depends-on]]
gnat_native = "^14"
gprbuild = "^22"
61 changes: 61 additions & 0 deletions spark/certificates/certificates.adb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
-- SPDX-License-Identifier: PMPL-1.0-or-later
--
-- SPARK companion body for certificates.ads
--
-- Implementation strategy
-- -----------------------
-- Most subprograms are expression functions defined in the spec, so the
-- body contains only:
-- - Get_Format_Extension's case body (mirroring Rust format_extension)
-- - The three ghost lemmas with null bodies; gnatprove derives their
-- conclusions by quantifier instantiation + the definitional
-- equivalences in the spec.

pragma Ada_2022;

package body Certificates
with SPARK_Mode => On
is

function Get_Format_Extension (F : Certificate_Format)
return Format_Extension is
begin
case F is
when Alethe => return Ext_Alethe;
when DRAT => return Ext_Drat;
when LRAT => return Ext_Lrat;
when Lean4_Kernel => return Ext_Lean4cert;
when Coq_Kernel => return Ext_Coqcert;
when TSTP => return Ext_Tstp;
end case;
end Get_Format_Extension;

-- ── Cap_At_Malformed ───────────────────────────────────────────────
-- Ghost lemma: derived from definitions of Has_Malformed +
-- Verify_Alethe (both in spec). Body is null; gnatprove closes
-- via quantifier duality (¬∀x.P(x) ↔ ∃x.¬P(x)).
procedure Cap_At_Malformed (Steps : Step_Array) is
begin
null;
end Cap_At_Malformed;

-- ── Empty_Verifies ─────────────────────────────────────────────────
-- Ghost lemma: vacuous-quantifier over an empty range. gnatprove
-- recognises the empty range and closes immediately.
procedure Empty_Verifies (Steps : Step_Array) is
begin
null;
end Empty_Verifies;

-- ── Format_Extension_Is_Injective ──────────────────────────────────
-- Ghost lemma: case-by-case on F1 (six values), and within each case
-- the case-arm post of Get_Format_Extension pins down the result, so
-- F2 /= F1 forces the other arm to be taken, yielding a different
-- Format_Extension. gnatprove discharges by exhaustive case-split.
procedure Format_Extension_Is_Injective
(F1, F2 : Certificate_Format) is
begin
null;
end Format_Extension_Is_Injective;

end Certificates;
137 changes: 137 additions & 0 deletions spark/certificates/certificates.ads
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
-- SPDX-License-Identifier: PMPL-1.0-or-later
--
-- SPARK companion spec for src/rust/verification/certificates.rs
--
-- Proved properties
-- 1. Get_Format_Extension is total over Certificate_Format and injective.
-- 2. Check_Alethe_Step is the exact boolean characterisation of the Rust
-- structural validator:
-- - Assume step: valid iff raw is non-empty
-- - Step step: valid iff raw has the :rule marker
-- (Issue #40 roadmap obligation: "Alethe / DRAT / TSTP / OpenTheory
-- cert parsing — totality + reject-malformed". The totality piece
-- is Get_Format_Extension; reject-malformed is encoded by the
-- check predicates + Cap_At_Malformed below.)
-- 3. Verify_Alethe returns true iff every step in the input array
-- passes Check_Alethe_Step (cap-at-malformed monotonicity).
-- 4. Cap_At_Malformed (ghost): any malformed step ⇒ Verify_Alethe = False.
-- 5. Empty_Verifies (ghost): empty input ⇒ Verify_Alethe = True (vacuous
-- quantifier over an empty range).
--
-- SPARK doesn't model strings, subprocess, or external checkers, so the
-- DRAT/LRAT/Lean4/Coq paths (which shell out to drat-trim / lean4checker /
-- coqchk) are outside this companion's scope. This companion mirrors the
-- pure-decision Alethe structural validator and the format-extension
-- mapping — the parts of certificates.rs that are purely deterministic
-- functions of their inputs and have no IO surface.

pragma Ada_2022;

package Certificates
with SPARK_Mode => On
is
-- ── CertificateFormat ──────────────────────────────────────────────
-- Mirrors Rust enum CertificateFormat (Rust line 20-33). Same
-- positional order so Ada and Rust integer encodings agree.
type Certificate_Format is
(Alethe, DRAT, LRAT, Lean4_Kernel, Coq_Kernel, TSTP);

-- ── Format extensions ──────────────────────────────────────────────
-- Rust format_extension returns `&str` literals. SPARK doesn't model
-- strings ergonomically, so we map each format to a tagged extension
-- enum. Injectivity is then expressible via the case post-condition.
type Format_Extension is
(Ext_Alethe, Ext_Drat, Ext_Lrat, Ext_Lean4cert, Ext_Coqcert, Ext_Tstp);

function Get_Format_Extension (F : Certificate_Format)
return Format_Extension
with Post =>
(case F is
when Alethe => Get_Format_Extension'Result = Ext_Alethe,
when DRAT => Get_Format_Extension'Result = Ext_Drat,
when LRAT => Get_Format_Extension'Result = Ext_Lrat,
when Lean4_Kernel => Get_Format_Extension'Result = Ext_Lean4cert,
when Coq_Kernel => Get_Format_Extension'Result = Ext_Coqcert,
when TSTP => Get_Format_Extension'Result = Ext_Tstp);

-- ── AletheStepKind ─────────────────────────────────────────────────
-- Mirrors Rust private enum AletheStepKind (Rust line 274).
type Alethe_Step_Kind is (Assume, Step);

-- ── AletheStep (abstracted) ────────────────────────────────────────
-- The Rust struct carries the raw string. SPARK can't reason about
-- arbitrary strings, so we abstract to two boolean attributes that
-- the Rust predicate Check_Alethe_Step consumes:
-- Raw_Is_Nonempty -- `!step.raw.is_empty()`
-- Has_Rule_Marker -- `raw.contains(":rule") && raw.starts_with("(step")`
-- A string-to-AletheStep adapter on the Rust side (not modeled here)
-- computes these two booleans at parse time.
type Alethe_Step is record
Kind : Alethe_Step_Kind := Assume;
Raw_Is_Nonempty : Boolean := False;
Has_Rule_Marker : Boolean := False;
end record;

-- ── Check_Alethe_Step ──────────────────────────────────────────────
-- Mirrors check_alethe_step (Rust line 226-234). Expression function
-- so the body IS the post-condition.
function Check_Alethe_Step (S : Alethe_Step) return Boolean is
(case S.Kind is
when Assume => S.Raw_Is_Nonempty,
when Step => S.Has_Rule_Marker);

-- ── Bounded step array ─────────────────────────────────────────────
-- Mirrors the Vec<AletheStep> produced by parse_alethe_steps. 65 536
-- caps Alethe proofs at an industrial scale (most CVC5 proofs run in
-- the hundreds-to-thousands range).
Max_Steps : constant := 65_536;
subtype Step_Index is Positive range 1 .. Max_Steps;
type Step_Array is array (Step_Index range <>) of Alethe_Step;

-- ── Verify_Alethe ──────────────────────────────────────────────────
-- Mirrors verify_alethe (Rust line 93-124): the result is valid iff
-- every step passes Check_Alethe_Step. Expression function — body
-- IS the post-condition; gnatprove discharges directly without VCs.
function Verify_Alethe (Steps : Step_Array) return Boolean is
(for all I in Steps'Range => Check_Alethe_Step (Steps (I)));

-- ── Has_Malformed (ghost) ──────────────────────────────────────────
function Has_Malformed (Steps : Step_Array) return Boolean is
(for some I in Steps'Range => not Check_Alethe_Step (Steps (I)))
with Ghost;

-- ── Cap_At_Malformed (ghost lemma) ─────────────────────────────────
-- Cap-at-malformed monotonicity: any single malformed step forces
-- the whole certificate to be rejected. Mirrors axiom_tracker's
-- cap-at-Reject and solver_integrity's cap-at-Tampered.
--
-- Proof: Has_Malformed (S) ≡ (exists I, ¬Check_Alethe_Step (S(I)))
-- ≡ ¬(forall I, Check_Alethe_Step (S(I)))
-- ≡ ¬Verify_Alethe (S).
procedure Cap_At_Malformed (Steps : Step_Array)
with Ghost,
Pre => Has_Malformed (Steps),
Post => not Verify_Alethe (Steps);

-- ── Empty_Verifies (ghost lemma) ───────────────────────────────────
-- An empty proof is vacuously valid: the universal quantifier over
-- an empty range is True. Stated explicitly to document the
-- pathological-but-correct edge case (parse_alethe_steps strips all
-- comments + empty lines; pure-comment input yields zero steps).
procedure Empty_Verifies (Steps : Step_Array)
with Ghost,
Pre => Steps'Length = 0,
Post => Verify_Alethe (Steps);

-- ── Format_Extension_Is_Injective (ghost lemma) ────────────────────
-- Distinct certificate formats map to distinct extensions. Documents
-- the storage-path uniqueness property used by store_certificate
-- (Rust line 182): different formats yield different filenames, so
-- two certificates of different formats never collide on disk.
procedure Format_Extension_Is_Injective
(F1, F2 : Certificate_Format)
with Ghost,
Pre => F1 /= F2,
Post => Get_Format_Extension (F1) /= Get_Format_Extension (F2);

end Certificates;
25 changes: 25 additions & 0 deletions spark/certificates/certificates.gpr
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- GPRbuild project for certificates SPARK companion
project Certificates is

for Source_Dirs use (".");
for Object_Dir use ".obj";
for Main use ();

package Compiler is
for Switches ("Ada") use
("-gnat2022",
"-gnatX",
"-O1",
"-gnatn");
end Compiler;

package Prove is
for Proof_Switches ("Ada") use
("--mode=prove",
"--level=1",
"--counterexamples=on",
"-j0",
"--report=all");
end Prove;

end Certificates;
114 changes: 114 additions & 0 deletions spark/certificates/certificates_proof.adb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
-- SPDX-License-Identifier: PMPL-1.0-or-later
--
-- Proof obligations and gnatprove discharge notes for certificates.
-- This file compiles to a no-op; all content is documentary.
--
-- Run proof:
-- cd spark/certificates
-- gnatprove --mode=prove --level=1 -P certificates.gpr

pragma SPARK_Mode (Off);

-- ════════════════════════════════════════════════════════════════════════
-- Proof obligation catalogue
-- ════════════════════════════════════════════════════════════════════════

-- PO-1 Certificate_Format / Format_Extension enumeration totality
-- ─────────────────────────────────────────────────────────────────────
-- Ada 2022 LRM 3.5.1. Definitional, no VC.
-- Status: DEFINITIONAL.

-- PO-2 Get_Format_Extension — exhaustive case-arm Post
-- ─────────────────────────────────────────────────────────────────────
-- VC: result matches the case arm corresponding to the input F.
--
-- Discharge: the body is a six-arm case with one return per arm; each
-- return matches exactly one Post case-arm. gnatprove substitutes the
-- path condition at the return site.
-- Tactic: --level=0.
-- Status: AUTO.

-- PO-3 Check_Alethe_Step expression function — Post
-- ─────────────────────────────────────────────────────────────────────
-- Expression function body IS the Post; definitional.
-- Status: DEFINITIONAL.

-- PO-4 Verify_Alethe expression function — Post
-- ─────────────────────────────────────────────────────────────────────
-- Expression function body IS the Post; definitional.
-- Status: DEFINITIONAL.

-- PO-5 Has_Malformed expression function — Post
-- ─────────────────────────────────────────────────────────────────────
-- Expression function body IS the Post; definitional.
-- Status: DEFINITIONAL.

-- PO-6 Cap_At_Malformed ghost lemma
-- ─────────────────────────────────────────────────────────────────────
-- VC: Has_Malformed (S) => ¬Verify_Alethe (S)
--
-- Discharge: by definition,
-- Has_Malformed (S) ≡ (exists I in S'Range, ¬Check_Alethe_Step (S(I)))
-- Verify_Alethe (S) ≡ (forall I in S'Range, Check_Alethe_Step (S(I)))
-- so Has_Malformed (S) ≡ ¬Verify_Alethe (S) by ¬∀ ↔ ∃¬.
-- CVC5/Z3 close via quantifier instantiation.
-- Tactic: --level=1.
-- Status: AUTO.

-- PO-7 Empty_Verifies ghost lemma
-- ─────────────────────────────────────────────────────────────────────
-- VC: Steps'Length = 0 => Verify_Alethe (Steps)
--
-- Discharge: (forall I in empty_range, P(I)) = True is a standard SMT
-- axiom for the empty range; gnatprove closes immediately.
-- Tactic: --level=0.
-- Status: AUTO.

-- PO-8 Format_Extension_Is_Injective ghost lemma
-- ─────────────────────────────────────────────────────────────────────
-- VC: F1 /= F2 => Get_Format_Extension (F1) /= Get_Format_Extension (F2)
--
-- Discharge: case-split on F1 (six arms); within each arm,
-- Get_Format_Extension (F1) is pinned to a specific Format_Extension by
-- the case-arm Post. F2 /= F1 means F2 falls in a different arm,
-- yielding a different Format_Extension. Six arms × five remaining
-- choices = 30 unsat sub-cases; gnatprove enumerates and closes each
-- by direct evaluation.
-- Tactic: --level=1.
-- Status: AUTO.

-- ════════════════════════════════════════════════════════════════════════
-- Equivalence argument: SPARK spec <=> Rust implementation
-- ════════════════════════════════════════════════════════════════════════
--
-- Mirrored decisions
-- - CertificateFormat enum (6 variants, same positional order)
-- - format_extension's six-arm match (Rust line 253-262)
-- - check_alethe_step's two-arm match on AletheStepKind (Rust line
-- 230-233), with the per-arm boolean predicates extracted as the
-- Raw_Is_Nonempty / Has_Rule_Marker abstractions
-- - verify_alethe's "scan all, any failure ⇒ invalid" decision
-- (Rust line 103-109), expressed as a universal quantifier
--
-- Out of scope (cannot be modeled in SPARK)
-- - DRAT/LRAT verification (Rust line 127-179): shells out to
-- drat-trim, depends on filesystem + subprocess + lossy parse of
-- stdout for "VERIFIED" substring.
-- - Lean4 / Coq kernel checks: also subprocess-based.
-- - Alethe parse_alethe_steps (Rust line 200-223): string-level
-- line.starts_with predicates; SPARK abstracts the result of
-- parsing into AletheStep records with the two booleans set.
-- - BLAKE3 hashing in ProofCertificate::new: opaque crypto.
--
-- Equivalence to the Rust verify_alethe is thus partial:
-- - Strong equivalence over the structural validator (PO-3 + PO-4
-- + PO-6 together encode the exact Rust decision function).
-- - The string-to-AletheStep adapter on the Rust side must compute
-- Raw_Is_Nonempty and Has_Rule_Marker faithfully; this is a
-- boundary obligation enforced by the AletheStep constructor on
-- the Rust side, not by SPARK.

procedure Certificates_Proof is
begin
null;
end Certificates_Proof;
Loading