Skip to content

Commit 54c03aa

Browse files
feat(spark): certificates SPARK companion — run #3 (completes initial roadmap) (#50)
## Summary SPARK 2014/2022 formal companion for `src/rust/verification/certificates.rs`, the Alethe / DRAT / TSTP / OpenTheory cert parsing layer. **Completes the initial 3-module Rust→Rust/SPARK roadmap from #40** (run #1 was axiom_tracker #47, run #2 was solver_integrity #49). - `spark/certificates/certificates.ads` — Ada spec for `Certificate_Format`, `Format_Extension`, `Alethe_Step`, `Verify_Alethe` + 3 ghost lemmas - `spark/certificates/certificates.adb` — body (`Get_Format_Extension` six-arm case; ghost lemmas have null bodies) - `spark/certificates/certificates_proof.adb` — 8 proof obligations - `spark/certificates/certificates.gpr`, `alire.toml`, `.gitignore` - `spark/README.adoc` — module index updated ## Proved properties | # | Property | Method | |---|---|---| | PO-1 | `Certificate_Format` / `Format_Extension` enumeration totality | Definitional | | PO-2 | `Get_Format_Extension` exhaustive case-arm Post | CVC5 path substitution | | PO-3 | `Check_Alethe_Step` exact characterisation (Assume ⇒ raw nonempty; Step ⇒ has `:rule`) | Expression function, definitional | | PO-4 | `Verify_Alethe` = ∀ step. valid | Expression function, definitional | | PO-5 | `Has_Malformed` ghost predicate | Expression function, definitional | | PO-6 | **`Cap_At_Malformed`** — any malformed step ⇒ `¬Verify_Alethe` | Quantifier duality, --level=1 | | PO-7 | `Empty_Verifies` — empty input vacuously valid | Empty-range axiom, --level=0 | | PO-8 | **`Format_Extension_Is_Injective`** — distinct formats ⇒ distinct extensions | Six-arm case-split (30 unsat subcases), --level=1 | ## gnatprove summary ``` Total 10 checks, 0 unproved, 0 justified Run-time Checks 2 (CVC5 75%, altergo 25%) Functional Contracts 4 (CVC5 56%, altergo 44%) Termination 4 (flow analysis) ``` Max steps: **230** (the case-split injectivity proof is the work-heaviest VC, as expected). Toolchain on the agent runner: gnatprove 14.1.1 (Why3 1.6.0, Alt-Ergo 2.4.0, CVC5 1.1.2, Z3) — wired via #40. ## Scope and out-of-scope **In scope (mirrored):** - `CertificateFormat` enum (6 variants, same positional order as Rust) - `format_extension` six-arm match (Rust line 253-262) - `check_alethe_step` two-arm match on `AletheStepKind` (Rust line 230-233) - `verify_alethe`'s scan-all-and-reject-on-any-failure decision (Rust line 103-109) **Out of scope (cannot be modeled in SPARK):** - `verify_drat` (Rust line 127-179): shells out to `drat-trim`, depends on filesystem + subprocess + stdout substring parse - Lean4 / Coq kernel checks: subprocess-based - `parse_alethe_steps`: string-level `line.starts_with` predicates; SPARK abstracts the parse result into `Alethe_Step` records with `Raw_Is_Nonempty` / `Has_Rule_Marker` set - BLAKE3 hashing in `ProofCertificate::new`: opaque crypto Equivalence to Rust `verify_alethe` is **strong over the structural validator**; the Rust-side parser-adapter must compute `Raw_Is_Nonempty` and `Has_Rule_Marker` faithfully at the boundary (a constructor obligation, not a SPARK obligation). ## Test plan - [x] `gnatprove --mode=prove --level=1 -P spark/certificates/certificates.gpr` — 10/0/0 locally - [x] Toolchain prereqs satisfied via #40 ## CI note Same pre-existing repo-level failures as #47 / #49 (cargo fmt diffs in untouched test files, rustdoc intra-doc-link breaks in `pvs.rs`, `Cargo.lock` v4 vs MSRV, missing `just` on MVP Smoke, missing `.clusterfuzzlite/Dockerfile`, ubuntu24 vs `erlef/setup-beam`). This PR adds only `spark/certificates/` and one line to `spark/README.adoc` — touches no Rust, Julia, Elixir, Dockerfile, or Cargo.lock. Expecting admin-merge with the same justification as the prior two SPARK runs. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 1f92de7 commit 54c03aa

7 files changed

Lines changed: 355 additions & 0 deletions

File tree

spark/README.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ Summary of SPARK analysis
5151
| `src/rust/integrity/solver_integrity.rs`
5252
| Run #2 — proved (11/0)
5353

54+
| link:certificates/[certificates]
55+
| `src/rust/verification/certificates.rs`
56+
| Run #3 — proved (10/0)
57+
5458
|===
5559

5660
== Adding a new companion

spark/certificates/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.obj/

spark/certificates/alire.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name = "certificates_spark"
2+
description = "SPARK companion for echidna certificates module"
3+
version = "0.1.0"
4+
licenses = "PMPL-1.0-or-later"
5+
maintainers = ["hyperpolymath@users.noreply.github.com"]
6+
maintainers-logins = ["hyperpolymath"]
7+
8+
[build-switches]
9+
"*".Ada_Version = "Ada2022"
10+
11+
[[depends-on]]
12+
gnat_native = "^14"
13+
gprbuild = "^22"
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
-- SPDX-License-Identifier: PMPL-1.0-or-later
2+
--
3+
-- SPARK companion body for certificates.ads
4+
--
5+
-- Implementation strategy
6+
-- -----------------------
7+
-- Most subprograms are expression functions defined in the spec, so the
8+
-- body contains only:
9+
-- - Get_Format_Extension's case body (mirroring Rust format_extension)
10+
-- - The three ghost lemmas with null bodies; gnatprove derives their
11+
-- conclusions by quantifier instantiation + the definitional
12+
-- equivalences in the spec.
13+
14+
pragma Ada_2022;
15+
16+
package body Certificates
17+
with SPARK_Mode => On
18+
is
19+
20+
function Get_Format_Extension (F : Certificate_Format)
21+
return Format_Extension is
22+
begin
23+
case F is
24+
when Alethe => return Ext_Alethe;
25+
when DRAT => return Ext_Drat;
26+
when LRAT => return Ext_Lrat;
27+
when Lean4_Kernel => return Ext_Lean4cert;
28+
when Coq_Kernel => return Ext_Coqcert;
29+
when TSTP => return Ext_Tstp;
30+
end case;
31+
end Get_Format_Extension;
32+
33+
-- ── Cap_At_Malformed ───────────────────────────────────────────────
34+
-- Ghost lemma: derived from definitions of Has_Malformed +
35+
-- Verify_Alethe (both in spec). Body is null; gnatprove closes
36+
-- via quantifier duality (¬∀x.P(x) ↔ ∃x.¬P(x)).
37+
procedure Cap_At_Malformed (Steps : Step_Array) is
38+
begin
39+
null;
40+
end Cap_At_Malformed;
41+
42+
-- ── Empty_Verifies ─────────────────────────────────────────────────
43+
-- Ghost lemma: vacuous-quantifier over an empty range. gnatprove
44+
-- recognises the empty range and closes immediately.
45+
procedure Empty_Verifies (Steps : Step_Array) is
46+
begin
47+
null;
48+
end Empty_Verifies;
49+
50+
-- ── Format_Extension_Is_Injective ──────────────────────────────────
51+
-- Ghost lemma: case-by-case on F1 (six values), and within each case
52+
-- the case-arm post of Get_Format_Extension pins down the result, so
53+
-- F2 /= F1 forces the other arm to be taken, yielding a different
54+
-- Format_Extension. gnatprove discharges by exhaustive case-split.
55+
procedure Format_Extension_Is_Injective
56+
(F1, F2 : Certificate_Format) is
57+
begin
58+
null;
59+
end Format_Extension_Is_Injective;
60+
61+
end Certificates;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
-- SPDX-License-Identifier: PMPL-1.0-or-later
2+
--
3+
-- SPARK companion spec for src/rust/verification/certificates.rs
4+
--
5+
-- Proved properties
6+
-- 1. Get_Format_Extension is total over Certificate_Format and injective.
7+
-- 2. Check_Alethe_Step is the exact boolean characterisation of the Rust
8+
-- structural validator:
9+
-- - Assume step: valid iff raw is non-empty
10+
-- - Step step: valid iff raw has the :rule marker
11+
-- (Issue #40 roadmap obligation: "Alethe / DRAT / TSTP / OpenTheory
12+
-- cert parsing — totality + reject-malformed". The totality piece
13+
-- is Get_Format_Extension; reject-malformed is encoded by the
14+
-- check predicates + Cap_At_Malformed below.)
15+
-- 3. Verify_Alethe returns true iff every step in the input array
16+
-- passes Check_Alethe_Step (cap-at-malformed monotonicity).
17+
-- 4. Cap_At_Malformed (ghost): any malformed step ⇒ Verify_Alethe = False.
18+
-- 5. Empty_Verifies (ghost): empty input ⇒ Verify_Alethe = True (vacuous
19+
-- quantifier over an empty range).
20+
--
21+
-- SPARK doesn't model strings, subprocess, or external checkers, so the
22+
-- DRAT/LRAT/Lean4/Coq paths (which shell out to drat-trim / lean4checker /
23+
-- coqchk) are outside this companion's scope. This companion mirrors the
24+
-- pure-decision Alethe structural validator and the format-extension
25+
-- mapping — the parts of certificates.rs that are purely deterministic
26+
-- functions of their inputs and have no IO surface.
27+
28+
pragma Ada_2022;
29+
30+
package Certificates
31+
with SPARK_Mode => On
32+
is
33+
-- ── CertificateFormat ──────────────────────────────────────────────
34+
-- Mirrors Rust enum CertificateFormat (Rust line 20-33). Same
35+
-- positional order so Ada and Rust integer encodings agree.
36+
type Certificate_Format is
37+
(Alethe, DRAT, LRAT, Lean4_Kernel, Coq_Kernel, TSTP);
38+
39+
-- ── Format extensions ──────────────────────────────────────────────
40+
-- Rust format_extension returns `&str` literals. SPARK doesn't model
41+
-- strings ergonomically, so we map each format to a tagged extension
42+
-- enum. Injectivity is then expressible via the case post-condition.
43+
type Format_Extension is
44+
(Ext_Alethe, Ext_Drat, Ext_Lrat, Ext_Lean4cert, Ext_Coqcert, Ext_Tstp);
45+
46+
function Get_Format_Extension (F : Certificate_Format)
47+
return Format_Extension
48+
with Post =>
49+
(case F is
50+
when Alethe => Get_Format_Extension'Result = Ext_Alethe,
51+
when DRAT => Get_Format_Extension'Result = Ext_Drat,
52+
when LRAT => Get_Format_Extension'Result = Ext_Lrat,
53+
when Lean4_Kernel => Get_Format_Extension'Result = Ext_Lean4cert,
54+
when Coq_Kernel => Get_Format_Extension'Result = Ext_Coqcert,
55+
when TSTP => Get_Format_Extension'Result = Ext_Tstp);
56+
57+
-- ── AletheStepKind ─────────────────────────────────────────────────
58+
-- Mirrors Rust private enum AletheStepKind (Rust line 274).
59+
type Alethe_Step_Kind is (Assume, Step);
60+
61+
-- ── AletheStep (abstracted) ────────────────────────────────────────
62+
-- The Rust struct carries the raw string. SPARK can't reason about
63+
-- arbitrary strings, so we abstract to two boolean attributes that
64+
-- the Rust predicate Check_Alethe_Step consumes:
65+
-- Raw_Is_Nonempty -- `!step.raw.is_empty()`
66+
-- Has_Rule_Marker -- `raw.contains(":rule") && raw.starts_with("(step")`
67+
-- A string-to-AletheStep adapter on the Rust side (not modeled here)
68+
-- computes these two booleans at parse time.
69+
type Alethe_Step is record
70+
Kind : Alethe_Step_Kind := Assume;
71+
Raw_Is_Nonempty : Boolean := False;
72+
Has_Rule_Marker : Boolean := False;
73+
end record;
74+
75+
-- ── Check_Alethe_Step ──────────────────────────────────────────────
76+
-- Mirrors check_alethe_step (Rust line 226-234). Expression function
77+
-- so the body IS the post-condition.
78+
function Check_Alethe_Step (S : Alethe_Step) return Boolean is
79+
(case S.Kind is
80+
when Assume => S.Raw_Is_Nonempty,
81+
when Step => S.Has_Rule_Marker);
82+
83+
-- ── Bounded step array ─────────────────────────────────────────────
84+
-- Mirrors the Vec<AletheStep> produced by parse_alethe_steps. 65 536
85+
-- caps Alethe proofs at an industrial scale (most CVC5 proofs run in
86+
-- the hundreds-to-thousands range).
87+
Max_Steps : constant := 65_536;
88+
subtype Step_Index is Positive range 1 .. Max_Steps;
89+
type Step_Array is array (Step_Index range <>) of Alethe_Step;
90+
91+
-- ── Verify_Alethe ──────────────────────────────────────────────────
92+
-- Mirrors verify_alethe (Rust line 93-124): the result is valid iff
93+
-- every step passes Check_Alethe_Step. Expression function — body
94+
-- IS the post-condition; gnatprove discharges directly without VCs.
95+
function Verify_Alethe (Steps : Step_Array) return Boolean is
96+
(for all I in Steps'Range => Check_Alethe_Step (Steps (I)));
97+
98+
-- ── Has_Malformed (ghost) ──────────────────────────────────────────
99+
function Has_Malformed (Steps : Step_Array) return Boolean is
100+
(for some I in Steps'Range => not Check_Alethe_Step (Steps (I)))
101+
with Ghost;
102+
103+
-- ── Cap_At_Malformed (ghost lemma) ─────────────────────────────────
104+
-- Cap-at-malformed monotonicity: any single malformed step forces
105+
-- the whole certificate to be rejected. Mirrors axiom_tracker's
106+
-- cap-at-Reject and solver_integrity's cap-at-Tampered.
107+
--
108+
-- Proof: Has_Malformed (S) ≡ (exists I, ¬Check_Alethe_Step (S(I)))
109+
-- ≡ ¬(forall I, Check_Alethe_Step (S(I)))
110+
-- ≡ ¬Verify_Alethe (S).
111+
procedure Cap_At_Malformed (Steps : Step_Array)
112+
with Ghost,
113+
Pre => Has_Malformed (Steps),
114+
Post => not Verify_Alethe (Steps);
115+
116+
-- ── Empty_Verifies (ghost lemma) ───────────────────────────────────
117+
-- An empty proof is vacuously valid: the universal quantifier over
118+
-- an empty range is True. Stated explicitly to document the
119+
-- pathological-but-correct edge case (parse_alethe_steps strips all
120+
-- comments + empty lines; pure-comment input yields zero steps).
121+
procedure Empty_Verifies (Steps : Step_Array)
122+
with Ghost,
123+
Pre => Steps'Length = 0,
124+
Post => Verify_Alethe (Steps);
125+
126+
-- ── Format_Extension_Is_Injective (ghost lemma) ────────────────────
127+
-- Distinct certificate formats map to distinct extensions. Documents
128+
-- the storage-path uniqueness property used by store_certificate
129+
-- (Rust line 182): different formats yield different filenames, so
130+
-- two certificates of different formats never collide on disk.
131+
procedure Format_Extension_Is_Injective
132+
(F1, F2 : Certificate_Format)
133+
with Ghost,
134+
Pre => F1 /= F2,
135+
Post => Get_Format_Extension (F1) /= Get_Format_Extension (F2);
136+
137+
end Certificates;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
-- GPRbuild project for certificates SPARK companion
2+
project Certificates is
3+
4+
for Source_Dirs use (".");
5+
for Object_Dir use ".obj";
6+
for Main use ();
7+
8+
package Compiler is
9+
for Switches ("Ada") use
10+
("-gnat2022",
11+
"-gnatX",
12+
"-O1",
13+
"-gnatn");
14+
end Compiler;
15+
16+
package Prove is
17+
for Proof_Switches ("Ada") use
18+
("--mode=prove",
19+
"--level=1",
20+
"--counterexamples=on",
21+
"-j0",
22+
"--report=all");
23+
end Prove;
24+
25+
end Certificates;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
-- SPDX-License-Identifier: PMPL-1.0-or-later
2+
--
3+
-- Proof obligations and gnatprove discharge notes for certificates.
4+
-- This file compiles to a no-op; all content is documentary.
5+
--
6+
-- Run proof:
7+
-- cd spark/certificates
8+
-- gnatprove --mode=prove --level=1 -P certificates.gpr
9+
10+
pragma SPARK_Mode (Off);
11+
12+
-- ════════════════════════════════════════════════════════════════════════
13+
-- Proof obligation catalogue
14+
-- ════════════════════════════════════════════════════════════════════════
15+
16+
-- PO-1 Certificate_Format / Format_Extension enumeration totality
17+
-- ─────────────────────────────────────────────────────────────────────
18+
-- Ada 2022 LRM 3.5.1. Definitional, no VC.
19+
-- Status: DEFINITIONAL.
20+
21+
-- PO-2 Get_Format_Extension — exhaustive case-arm Post
22+
-- ─────────────────────────────────────────────────────────────────────
23+
-- VC: result matches the case arm corresponding to the input F.
24+
--
25+
-- Discharge: the body is a six-arm case with one return per arm; each
26+
-- return matches exactly one Post case-arm. gnatprove substitutes the
27+
-- path condition at the return site.
28+
-- Tactic: --level=0.
29+
-- Status: AUTO.
30+
31+
-- PO-3 Check_Alethe_Step expression function — Post
32+
-- ─────────────────────────────────────────────────────────────────────
33+
-- Expression function body IS the Post; definitional.
34+
-- Status: DEFINITIONAL.
35+
36+
-- PO-4 Verify_Alethe expression function — Post
37+
-- ─────────────────────────────────────────────────────────────────────
38+
-- Expression function body IS the Post; definitional.
39+
-- Status: DEFINITIONAL.
40+
41+
-- PO-5 Has_Malformed expression function — Post
42+
-- ─────────────────────────────────────────────────────────────────────
43+
-- Expression function body IS the Post; definitional.
44+
-- Status: DEFINITIONAL.
45+
46+
-- PO-6 Cap_At_Malformed ghost lemma
47+
-- ─────────────────────────────────────────────────────────────────────
48+
-- VC: Has_Malformed (S) => ¬Verify_Alethe (S)
49+
--
50+
-- Discharge: by definition,
51+
-- Has_Malformed (S) ≡ (exists I in S'Range, ¬Check_Alethe_Step (S(I)))
52+
-- Verify_Alethe (S) ≡ (forall I in S'Range, Check_Alethe_Step (S(I)))
53+
-- so Has_Malformed (S) ≡ ¬Verify_Alethe (S) by ¬∀ ↔ ∃¬.
54+
-- CVC5/Z3 close via quantifier instantiation.
55+
-- Tactic: --level=1.
56+
-- Status: AUTO.
57+
58+
-- PO-7 Empty_Verifies ghost lemma
59+
-- ─────────────────────────────────────────────────────────────────────
60+
-- VC: Steps'Length = 0 => Verify_Alethe (Steps)
61+
--
62+
-- Discharge: (forall I in empty_range, P(I)) = True is a standard SMT
63+
-- axiom for the empty range; gnatprove closes immediately.
64+
-- Tactic: --level=0.
65+
-- Status: AUTO.
66+
67+
-- PO-8 Format_Extension_Is_Injective ghost lemma
68+
-- ─────────────────────────────────────────────────────────────────────
69+
-- VC: F1 /= F2 => Get_Format_Extension (F1) /= Get_Format_Extension (F2)
70+
--
71+
-- Discharge: case-split on F1 (six arms); within each arm,
72+
-- Get_Format_Extension (F1) is pinned to a specific Format_Extension by
73+
-- the case-arm Post. F2 /= F1 means F2 falls in a different arm,
74+
-- yielding a different Format_Extension. Six arms × five remaining
75+
-- choices = 30 unsat sub-cases; gnatprove enumerates and closes each
76+
-- by direct evaluation.
77+
-- Tactic: --level=1.
78+
-- Status: AUTO.
79+
80+
-- ════════════════════════════════════════════════════════════════════════
81+
-- Equivalence argument: SPARK spec <=> Rust implementation
82+
-- ════════════════════════════════════════════════════════════════════════
83+
--
84+
-- Mirrored decisions
85+
-- - CertificateFormat enum (6 variants, same positional order)
86+
-- - format_extension's six-arm match (Rust line 253-262)
87+
-- - check_alethe_step's two-arm match on AletheStepKind (Rust line
88+
-- 230-233), with the per-arm boolean predicates extracted as the
89+
-- Raw_Is_Nonempty / Has_Rule_Marker abstractions
90+
-- - verify_alethe's "scan all, any failure ⇒ invalid" decision
91+
-- (Rust line 103-109), expressed as a universal quantifier
92+
--
93+
-- Out of scope (cannot be modeled in SPARK)
94+
-- - DRAT/LRAT verification (Rust line 127-179): shells out to
95+
-- drat-trim, depends on filesystem + subprocess + lossy parse of
96+
-- stdout for "VERIFIED" substring.
97+
-- - Lean4 / Coq kernel checks: also subprocess-based.
98+
-- - Alethe parse_alethe_steps (Rust line 200-223): string-level
99+
-- line.starts_with predicates; SPARK abstracts the result of
100+
-- parsing into AletheStep records with the two booleans set.
101+
-- - BLAKE3 hashing in ProofCertificate::new: opaque crypto.
102+
--
103+
-- Equivalence to the Rust verify_alethe is thus partial:
104+
-- - Strong equivalence over the structural validator (PO-3 + PO-4
105+
-- + PO-6 together encode the exact Rust decision function).
106+
-- - The string-to-AletheStep adapter on the Rust side must compute
107+
-- Raw_Is_Nonempty and Has_Rule_Marker faithfully; this is a
108+
-- boundary obligation enforced by the AletheStep constructor on
109+
-- the Rust side, not by SPARK.
110+
111+
procedure Certificates_Proof is
112+
begin
113+
null;
114+
end Certificates_Proof;

0 commit comments

Comments
 (0)