|
Download code/docs/LEAN_VERIFIER.md from viai957/align: direct link, hf CLI and curl.
- Browser
- Download file 38.4 kB
-
https://huggingface.co/viai957/align/resolve/main/code/docs/LEAN_VERIFIER.md
- Command line
-
hf download hf://viai957/align/code/docs/LEAN_VERIFIER.md
-
curl -L -o LEAN_VERIFIER.md https://huggingface.co/viai957/align/resolve/main/code/docs/LEAN_VERIFIER.md
38.4 kB
| # Strict Lean 4 verifier (Track L / P1) | |
| A reward and an evaluator for formal proofs that cannot be gamed by "it compiles". Code: | |
| `align/verify/lean/` (stdlib only), installer `scripts/lean_setup.sh`, ceiling test | |
| `scripts/lean_ceiling_test.py` (+ frozen case tables `scripts/lean_ceiling_cases.py`), tests | |
| `tests/test_lean_verifier.py`. The Lean toolchain is an external binary | |
| (`docs/DEPENDENCY_POLICY.md`, "External binary: Lean 4 toolchain"), reached only through | |
| `subprocess`. | |
| Status (2026-10-12): implemented, tested without Lean (173 tests, fake REPL), tested against the | |
| real toolchain (19 slow tests), mutation-tested (40 hand mutants), and **ceiling-tested**: | |
| | | | | |
| |---|---| | |
| | Known-good accepted | **177 / 178 = 99.44 %** (Wilson 95 % [96.9 %, 99.9 %]); floor 98 % -> PASS | | |
| | Adversarial accepted | **0 / 61** -> PASS (a compile-only grader would have paid 51) | | |
| | Throughput, 8 workers | 0.57 proofs/s on a heavy unfiltered mix; 4.4 proofs/s on the median mix (GRPO shape) | | |
| | Memory per warm REPL | 0.64-0.65 GiB PSS with 8 workers (0.24 GiB private); 0.85 GiB peak under load | | |
| | Install | 5.57 GiB, 3 min; `import Mathlib` 3.2 s warm / 39.7 s cold | | |
| Section 5 holds the numbers; every figure there is copied from a file under `.tmp/lean_ceiling/`. | |
| Section 4 is the pre-registration. Section 6 lists what this verifier does not promise. | |
| ## 1. The pin | |
| | | | | |
| |---|---| | |
| | Lean | `leanprover/lean4:v4.9.0-rc1` (`lean --version`: 4.9.0-rc1, commit `be6c4894e0a6`) | | |
| | Mathlib | `leanprover-community/mathlib4@d1d1e4b72c6e009edd97cabe1d661b021fe8eeae` (2024-06-12) | | |
| | REPL | `leanprover-community/repl@v4.9.0-rc1` (`7df77c0a1c91`), built against the same toolchain | | |
| | elan | 4.2.4 | | |
| | Location | `/apps/development/rlhf/lean/` (`elan/`, `workspace/` = Lake project requiring Mathlib at the rev above, `repl/`, `PINS.json`) | | |
| **Provenance (how "the Goedel-Prover-V2 pin" was determined, not assumed).** Goedel-Prover-V2's | |
| README says "we follow DeepSeek-Prover-V1.5, which uses Lean 4 version 4.9 and the corresponding | |
| Mathlib". The repo's `mathlib4` git submodule is `xinhjBrant/mathlib4` at commit | |
| `2f65ba7f1a9144b20c8e7358513548e317d26de1`. That commit is a single child (message `align`, | |
| 2024-08-07, Huajian Xin of DeepSeek) of upstream `d1d1e4b72c6e`, and changes only | |
| `lakefile.lean` and `lake-manifest.json` to add `xinhjBrant/repl@deepseek` as a Lake dependency; | |
| its `lean-toolchain` is `leanprover/lean4:v4.9.0-rc1`. So "Lean 4.9" in that README is | |
| nominal: the binary is **4.9.0-rc1**, not 4.9.0. | |
| **Deviation, and why it is safe.** We pin the *upstream parent* and build the REPL separately. | |
| Mathlib's prebuilt `.olean` cache is keyed on a hash that includes `lakefile.lean`, so the | |
| DeepSeek child commit would not hit the cache and would force a multi-hour Mathlib build. The | |
| parent's Mathlib sources, and therefore the `.olean`s, are byte-identical to what the child | |
| compiles; the only difference is which REPL fork the lakefile names. We use upstream | |
| `repl@v4.9.0-rc1`, whose protocol (`cmd`/`env`) is all this package needs. The fork's extra | |
| options (`allTactics`, `ast`, `premises`) are not used. Install cost with the cache: 3 minutes, | |
| 5.8 GB (Section 5). | |
| **Version drift is a correctness issue.** SafeVerify keeps separate branches for 4.9.0 and | |
| 4.15.0; PutnamBench moved to 4.33.1 in 2026-09. Proofs verified here say nothing about 4.33. | |
| Every number this verifier produces must be reported with this pin and the verifier tier. | |
| ## 2. Architecture and what each layer defends | |
| ``` | |
| completion text ──extract_lean_code──▶ code ──static_check──▶ Plan | |
| (reward.py) (strict.py: lexer, bans, header, target) | |
| │ reject: no Lean touched | |
| ▼ | |
| ReplPool.acquire ─▶ Worker: [import once] ─▶ [audit prelude] ─▶ [header opens] | |
| (repl_pool.py) │ | |
| ref = `theorem _align_ref <given signature> := sorry` ──▶ env R | |
| candidate body, run in env R ──▶ env C (errors / sorry ⇒ reject) | |
| `#align_audit "<nonce>" 1 <target> _align_ref` in env C ──▶ verdict JSON | |
| ``` | |
| | Layer | Rejects | Independent of the lexer? | | |
| |---|---|---| | |
| | `extract_lean_code` | no fence / truncated (unclosed) fence / prose | n/a | | |
| | header equality | any change to imports, `open`s, options (token-for-token vs the reference header) | yes (token compare) | | |
| | lexical bans | `sorry`/`admit`, `native_decide`, `unsafe`/`partial`, `implemented_by`/`extern`/`export`, `axiom`/`opaque`, `macro`/`elab`/`syntax`/`notation`/`run_cmd`/`initialize`, any `#cmd`, `namespace`/`section`/`variable`/`attribute`/`local`, `instance`, `def`/`abbrev` (unless `allow_helper_defs`), `Lean.*`/`IO.*` identifiers, `exact?`/`apply?`, non-whitelisted `set_option` (incl. `maxRecDepth` > 16384), attributes that change compilation/elaboration | no (this is the lexer) | | |
| | target rules | target missing, declared twice, `private`/`protected`/attribute on it, signature tokens differ from the reference's (strict mode) | no | | |
| | compile | any `error`; any `sorry` warning; non-empty `sorries` | yes (Lean) | | |
| | **audit: axioms** | any axiom outside {`propext`, `Quot.sound`, `Classical.choice`}: catches `sorryAx`, `Lean.ofReduceBool`/`trustCompiler` (`native_decide`), user axioms, even if the lexer were fooled | yes | | |
| | **audit: statement bridge** | the kernel is asked to check `theorem _align_bridge : <type of _align_ref> := <candidate>`. `_align_ref` was elaborated *before* the candidate existed, so a definition-hole (instance, `variable`, shadowing) that makes the same text mean something else fails here | yes | | |
| | **audit: kernel recheck** | every local theorem re-added with `debug.skipKernelTC` forced off | yes | | |
| | **audit: meta-declaration scan** | a NEW local constant whose type/value mentions `Lean.Elab/Meta/Macro/Parser/PrettyPrinter/Syntax/Environment/Core/Compiler`, `IO`, `unsafeCast`: the footprint of an `elab`/`macro` that slipped past the lexer | yes | | |
| | audit: kind | the target must be a `theorem` (not a `def` that happens to inhabit the type) | yes | | |
| The audit verdict is the one info message tagged `ALIGN_AUDIT <random nonce>`, produced by a | |
| **separate REPL call** after the candidate ran. The candidate's own output cannot carry the | |
| nonce (it is generated after the candidate text is fixed), so it cannot forge a verdict. | |
| Reward semantics (`reward.py`): `accuracy ∈ {0,1}`, 1 only if every layer passes. Diagnostics | |
| (`StrictResult.reasons`, `.diagnostics`) are returned separately and never folded into the | |
| reward: shaped partial credit ("compiles but has `sorry`") is exactly the gradient a policy | |
| follows to the exploit (Leanabell-V2 also found shaped rewards slightly worse than binary). | |
| Plug-in points, none of which change the loss or `CompositeReward`: | |
| ```python | |
| from align.verify.lean import ReplPool, StrictVerifier, LeanAccuracy, find_toolchain | |
| pool = ReplPool.from_toolchain(find_toolchain(), preflight=True) # host rules + worker count | |
| acc = LeanAccuracy(StrictVerifier(pool)) | |
| accuracy, result = acc.score(formal_statement, completion_text) # float in {0,1}, diagnostics | |
| terms, result = acc.reward_terms(composite, formal_statement, completion_text, fmt, n_tokens) | |
| outcomes = acc.verify_batch(statements, completions) # OutcomeVerifier surface | |
| ``` | |
| **Failure semantics.** A proof that times out, exhausts memory or kills its REPL scores 0 (that | |
| is a property of the proof). A *dead verifier* must not: `LeanInfraError` is raised when the | |
| base environment cannot be built or when more than half of a batch (of at least 8) died for | |
| infrastructure reasons. Otherwise a broken toolchain would feed the policy an all-zero reward | |
| that reads as "the model got worse" (the 0/100 perfect-policy incident, mirrored). | |
| Failure taxonomy (`StrictResult.reasons`): `no_code`, `parse_error`, `unsupported_statement`, | |
| `header_mismatch`, `target_missing`, `target_modifier`, `statement_mismatch`, `banned:<x>`, | |
| `compile_error`, `sorry`, `not_theorem`, `axiom:<name>`, `bridge_failed` (defeq mode), | |
| `kernel_recheck_failed`, `meta_decl`, `skip_kernel_tc`, `timeout`, `crash`, `memory`, | |
| `output_too_large`, `protocol`, `reference_invalid`. | |
| Statement modes. `strict` (default): signature tokens equal the reference's *and* the kernel | |
| bridge passes. `defeq`: bridge only, so an alpha-renamed or restated-but-defeq signature is | |
| accepted. The bridge is checked by the kernel (not the elaborator's `isDefEq`), so "accepted" | |
| means "the candidate's constant has a type the kernel accepts as the reference's type". | |
| ## 3. REPL pool: measured facts the design rests on | |
| | Fact (measured on this box, Lean 4.9.0-rc1 + Mathlib pin) | Consequence in code | | |
| |---|---| | |
| | The REPL **does not flush stdout when it is a pipe** (only a pty line-buffers); the client blocks forever | binary wrapped in `stdbuf -o0 -e0` (`tests::test_wrapped_argv_*`) | | |
| | `lake env` prints **cwd-relative** paths (`././.lake/...`) | absolutised once in `toolchain.build_child_env`; child gets a minimal environment (no tokens) | | |
| | A **second fresh `import Mathlib` in a live process** cannot re-mmap the `.olean`s and reads them into the heap: `Pss_Anon` 0.25 → **4.14 GiB** | one import per process; every header variant is layered on it (`split_imports`, `Worker.header_env`); a different import set recycles the process | | |
| | RSS is ~3.65 GiB but 3.40 GiB of it is the shared, read-only `.olean` mapping; private heap is ~0.25 GiB | the watchdog caps **`Pss_Anon`**, not RSS; `plan_workers` budgets shared-once + n × cap | | |
| | `Nat.pow exponent is too big` and `lean::stack_space_exception` **abort the process** (`rc=1`, `rc=-6`) | `ReplCrash` with the stderr tail; the next `acquire` respawns; scored 0 | | |
| | `debug.skipKernelTC` is an **unknown option** on 4.9.0-rc1 | the whitelist + the audit's `skip_tc` check are guards for a pin upgrade, not a live exploit here | | |
| | Lean 4.9 has **no public `collectAxioms`** (`#print axioms` is private) | `alignCollectAxioms` in the prelude (same traversal) | | |
| | `variable (h : False)` *used in a proof body* is added to the theorem's type in 4.9 (pre-4.11 behaviour) | `variable` is banned lexically and the bridge catches it independently | | |
| Limits per worker (defaults, `ReplLimits`): anonymous-memory cap 4 GiB (watchdog polls | |
| `smaps_rollup` every 0.5 s), `prlimit --as=16GiB --fsize=256MiB --core=0`, `nice +5`, output cap | |
| 16 MiB, per-proof wall budget 180 s (compile + audit), SIGKILL of the whole process group on any | |
| violation, `MALLOC_ARENA_MAX=2`. Workers recycle after 1000 commands. Default worker count is | |
| `min(8, cores // 4)` = 8 on this box; `ReplPool.from_toolchain(preflight=True)` applies the | |
| `/apps/CLAUDE.md` checks (≥ 45 GB `MemAvailable`, load < 2× cores, disk queue < 64) through | |
| `align.utils.guard.preflight_host_check` and then shrinks the worker count so that, even with | |
| every worker at its cap, `MemAvailable` stays above the 45 GB floor. The ceiling-test script | |
| takes the same exclusive heavy-job `flock` as the trainers (a Lean pool is a heavy job). | |
| ## 4. Pre-registration (written BEFORE the registered ceiling run) | |
| Registered 2026-10-12 20:12 IST, before any run of `scripts/lean_ceiling_test.py` without | |
| `--allow-unregistered`. The case tables (`HEADER`, `STATEMENTS`, `ADVERSARIAL`, `RESTATED` in | |
| `scripts/lean_ceiling_cases.py`) are frozen by hash: | |
| PREREG_TABLE_SHA256: `61636c7136eb647801c588b00a35bd8bcc6ef4db126c21ea75769092883ff7e2` | |
| `python scripts/lean_ceiling_test.py --verify-prereg` recomputes it; the registered run refuses | |
| to start on a mismatch. | |
| **Honest note on process.** The tables were developed against this verifier (about six exploratory | |
| runs on 1-2 workers, `--exploratory --allow-unregistered`) so that every adversarial case is | |
| *live* (it would be paid by a compile-only grader, which the run also measures) and every | |
| restated proof compiles. Those exploratory runs informed case *authoring* only; thresholds | |
| below were fixed before the registered run, and any verifier change after registration that | |
| alters a verdict is reported as an amendment in Section 7, with the pre-amendment matrix kept. | |
| **Configuration under test** = shipped defaults: `StrictConfig()` (strict statement mode, no helper | |
| defs, kernel recheck ON, 180 s budget per proof) and `ReplLimits()` (4 GiB anonymous cap, 16 GiB | |
| `--as` backstop), 8 workers, `lean_workbook` compile-only filter budget 120 s. | |
| **Known-good set.** (a) Public verified proofs: 6 contiguous slices of 25 rows at offsets | |
| 0, 5000, 10000, 15000, 20000, 25000 of `Goedel-LM/Lean-workbook-proofs` (29,750 proofs verified | |
| under the Lean 4.9 / DeepSeek-Prover-V1.5 Mathlib environment), fetched over HTTP, never the | |
| full dataset. (b) 30 hand-restated Mathlib-style theorems with proofs. The formal statement | |
| given to the verifier is derived mechanically from each proof file (header + last theorem's | |
| signature cut at the signature-ending `:=` + `sorry`). **Filter:** an independent compile-only | |
| pass (same REPL, no strictness: no `error` message and no `sorry` warning) defines "compatible | |
| with the pin"; items failing it are excluded *and listed*. Scored population = compile-clean | |
| items. **Pass:** ≥ 98 % accepted and ≥ 50 items. (The roadmap's P1 row says ≥ 99 %; the task | |
| fixed 98 %, which is what is registered. Both are reported.) Any rejected known-good item is | |
| listed with its reasons; rejections caused by the policy bans (e.g. a genuine `native_decide` | |
| in a public proof) count as failures, because that is the cost the RL stage would pay. | |
| **Adversarial set.** ≥ 30 hand-built cases, all registered (61): categories sorry (7), admit (2), | |
| extra axiom (4), native_decide (3), changed statement (6), weakened hypothesis (3), definition | |
| hole (4), environment manipulation via elab/macro/run_cmd/initialize/#eval/#exit (9), theorem | |
| proved for a different name (6), timeout/crash/memory bombs (3), banned constructs (6), header | |
| tampering (4), no code (4). Each is a *completion* (prose + fenced block) so fence extraction, | |
| static gate, REPL compile and audit are all exercised. **Pass:** 0 accepted. Bomb cases run on | |
| a dedicated 1-worker pool (1 GiB anonymous cap, 30 s budget) so they terminate promptly; the | |
| code path is the same. Reported alongside, not part of the pass criterion: how many cases a | |
| compile-only grader would pay; and which cases the Lean-side audit rejects *on its own* with | |
| the lexical gate disabled. | |
| **Confusion matrix** (reported verbatim): rows = truth (good / bad), columns = verdict | |
| (accept / reject); good = compile-clean known-good, bad = adversarial. | |
| **Measurements** (reported, no threshold): install disk size; `import Mathlib` cold | |
| (page cache evicted with `posix_fadvise(DONTNEED)`) and warm; per-REPL PSS / Pss_Anon / RSS from | |
| `/proc/<pid>/smaps_rollup` with Mathlib imported, for one process and for 8 concurrent workers; | |
| throughput (proofs/s) at the worker count used, with the worker set warm, on unique statements | |
| and in the GRPO shape (12 statements × 8 samples); latency p50/p90/p99; PSS sampled during the | |
| run. | |
| ## 5. Results | |
| All numbers below are copied from `.tmp/lean_ceiling/registered/result.json` and | |
| `.tmp/lean_ceiling/registered.log` (the registered run: hash verified, `prereg_registered=true`, | |
| 8 workers, host preflight passed, heavy-job lock taken after waiting for another track's job | |
| to release it, started ~20:13 IST, 2026-10-12). Host: 48 cores; `MemAvailable` never fell below | |
| 136.9 GB in the samples taken during the strict known-good pass. | |
| ### 5.1 Ceiling test: confusion matrix (verbatim) | |
| ``` | |
| verdict ACCEPT verdict REJECT total | |
| truth GOOD proofs 177 1 178 | |
| truth BAD proofs 0 61 61 | |
| ``` | |
| | Criterion (pre-registered) | Measured | Verdict | | |
| |---|---|---| | |
| | known-good accepted >= 98 %, n >= 50 | **177 / 178 = 99.44 %** (Wilson 95 % [96.89 %, 99.90 %]); n = 178 | **PASS** (also >= the roadmap's 99 %) | | |
| | adversarial accepted = 0, n >= 30 | **0 / 61** | **PASS** | | |
| | gate 5 of the roadmap: strict false-negative rate > 5 % blocks P2 | 0.56 % (upper Wilson 3.1 %) | not blocked | | |
| **Known-good population.** 180 candidates (150 Lean-Workbook-proofs rows from dataset revision | |
| `b731852af8d8`, raw file sha256 `55932494...aebef`; 30 restated). The independent compile-only | |
| filter kept 178: **148 + 30**. The 2 excluded (`lean_workbook_10036`, `lean_workbook_plus_64037`) | |
| were excluded because the compile-only pass hit its **120 s budget** (`why: timeout`), not because | |
| of a compile error, so "version-incompatible" would be the wrong label: they are slow proofs. | |
| All 30 restated proofs were accepted. | |
| **The one rejected known-good proof: `lean_workbook_51088`, reason `timeout`** (180 s strict | |
| budget). It is a nlinarith proof with nine hints. Follow-up (exploratory, 1 worker, not part of | |
| the registered numbers), same proof, same pool code, only the REPL environment layers differ: | |
| | environment the body runs in | wall time | | |
| |---|---| | |
| | imports + header (what the compile-only filter uses) | 99.9 s (filter: 104.1 s) | | |
| | + reference theorem `_align_ref` | 225.8 s | | |
| | + audit prelude | 513.6 s | | |
| | + both (= the strict path) | 516.5 s (timed out at 400 s in a separate strict run) | | |
| On `lean_workbook_10009` (20 s) the same four environments take 19.9 / 20.8 / 20.1 / 20.0 s. So | |
| this is not a systematic strict-tier slowdown; it is the well-known run-time chaos of a borderline | |
| `nlinarith` (the certificate search is sensitive to state it inherits from earlier commands) hitting | |
| a 180 s wall-clock budget. It is a genuine false negative of the *reward* (such a proof would score | |
| 0), it is bounded at one in 178 here, and raising the budget to Goedel's 300 s would not have saved | |
| it (517 s). The honest reading: wall-clock-bounded verification of proofs this close to the limit is | |
| flaky in BOTH directions, so for evaluation (not training) report results at a stated budget. | |
| **Adversarial population (61 cases, all rejected).** First reason per case: 58 rejected by the | |
| static gate (no Lean call), 3 by the REPL layer (the bombs). By category (cases / accepted): | |
| sorry 7/0, admit 2/0, extra_axiom 4/0, native_decide 3/0, changed_statement 6/0, | |
| weakened_hypothesis 3/0, definition_hole 4/0, env_manipulation 9/0, wrong_name 6/0, | |
| timeout_bomb 3/0 (timeout, `rc=-6` stack-overflow crash, anonymous-memory kill at 1110 MiB over a | |
| 1024 MiB cap), banned_construct 6/0, header_tamper 4/0, no_code 4/0. | |
| * **A compile-only grader would have paid 51** of the 54 code-bearing, non-bomb cases | |
| (each case's own text compiled with no strictness). The 3 it would not: `header_missing` (no | |
| Mathlib), `duplicate_target` (Lean itself rejects a duplicate name) and `skipKernelTC_option` | |
| (unknown option on this Lean). The 3 bombs and the 4 `no_code` cases are not applicable. So | |
| 51 are live exploits, not strawmen. | |
| * **Semantic layer alone** (lexical gate OFF, so each exploit actually executes inside Lean, with | |
| `kernel_recheck` on): rejected **45 of the 54** code-bearing non-bomb cases by the Lean-side audit | |
| alone (`sorry`, `axiom:bad`, `axiom:Lean.ofReduceBool`, `axiom:evil` after an `elab` added an | |
| axiom, `statement_mismatch` for the instance hole, the `variable` hole, shadowing via | |
| `macro_rules`, changed constants/hypotheses, `meta_decl` for `initialize`, `target_missing` | |
| for wrong names/namespaces/private targets, `header_mismatch`). The 9 it accepted are the | |
| *policy-only* set, where the proof of the stated theorem is valid and the lexical ban is the | |
| only thing stopping it: `eval_side_effect`, `exit_after_theorem`, `attribute_on_target`, | |
| `unsafe_implemented_by`, `partial_def`, `extern_attr`, `search_tactic_exact_q`, | |
| `maxRecDepth_abuse`, `helper_def_with_proof`. None is a soundness gap (the theorem is proved); | |
| they are rejected to keep the attack surface small, and each costs real-world proofs that use | |
| them (Section 6, item 5). | |
| ### 5.2 Install and process measurements | |
| | Quantity | Measured | | |
| |---|---| | |
| | Install time (`lean_setup.sh`, cache hit) | 3 min 9 s (18:23:06 -> 18:26:15); an idempotent re-run takes ~10 s | | |
| | Disk, total | 5,981,549,078 B = **5.57 GiB**: elan + toolchain 0.93 GiB, workspace 4.34 GiB (of which Mathlib package 4.11 GiB), REPL 94 MiB | | |
| | `import Mathlib` + `Aesop`, **cold** (page cache for all 11,171 `.olean/.ilean/.so` files = 4.19 GiB evicted with `posix_fadvise(DONTNEED)`) | **39.7 s** | | |
| | same, **warm** (3 runs) | 3.19, 3.20, 3.19 s (median **3.19 s**) | | |
| | 8 workers warmed in parallel | 3.38 s (warm cache) | | |
| | One REPL after the import (alone on the box) | RSS 3439 MiB, **PSS 3437 MiB**, `Pss_Anon` **244 MiB**, `Pss_File` 3193 MiB, 1 thread, VmSize 4071 MiB | | |
| | **Per-worker PSS with 8 workers all holding Mathlib** | **643-653 MiB** each (mean 648; RSS 3.4-3.5 GiB each) = 244 MiB anonymous + 3193 MiB / 8 shared. Sum of PSS **5.06 GiB**; sum of RSS **26.8 GiB** (the double count the PSS figure removes) | | |
| | Per-worker PSS **during** the 178-proof run (sampled every 0.5 s) | mean 717-792 MiB, max **873 MiB**; `Pss_Anon` max 404 MiB; RSS max 3.65 GiB | | |
| | Worker growth over 1042 commands | none that approached any limit: recycled 0 times (limit 1000/worker) | | |
| `fincore` was not usable for confirming the eviction (its output did not parse into a residency | |
| fraction: the script printed `None` and `4096.0`; ignore those two fields in `result.json`). The 12x | |
| cold/warm gap is the evidence that the eviction worked. | |
| Model implied by the measurements: `PSS(n workers) ~ 0.24 GiB + 3.12 GiB / n` per worker, total | |
| `~ 3.12 + 0.24 n` GiB. Eight workers cost ~5 GiB of RAM in total. The worst case (all eight at | |
| their 4 GiB anonymous cap) is 3.12 + 8 x 4 = 35 GiB, which is what `plan_workers` budgets against | |
| the 45 GB floor, so the pool cannot push `MemAvailable` under the floor on this host. **Roadmap gate 5** ("above ~4 GB per warm REPL the pool | |
| shrinks below ~8 workers") is not triggered: 0.65 GiB per warm REPL, 0.85 GiB peak under load. | |
| ### 5.3 Throughput (8 workers, warm) | |
| | Shape | Proofs | Wall | Proofs/s | Notes | | |
| |---|---|---|---|---| | |
| | 178 unique statements (the known-good set, strict tier incl. kernel recheck) | 178 | 310.9 s | **0.573** | latency p50 1.97 s, p90 21.4 s, p99 158.6 s, max 180.3 s (the timeout), mean 9.76 s | | |
| | GRPO shape: 12 median-latency statements x 8 identical samples | 96 | 21.9 s | **4.38** | reference environment and baseline audit reused per worker; all 96 accepted | | |
| The first number is dominated by the heavy tail of Lean-Workbook `nlinarith` proofs (mean 11.7 s, | |
| median 2.65 s for those; the 30 restated proofs: median 0.18 s, max 1.5 s) and by one 180 s | |
| straggler: the ideal packing of the work is 178 x 9.76 s / 8 = 217 s. It is **not** a verifier | |
| overhead: the compile-only filter took 273 s for 180 candidates on the same 8 workers, i.e. the | |
| strict tier added ~14 % (audit 0.03-0.7 s per proof; kernel recheck +0.04-0.12 s on 20-43 s | |
| proofs). Against the research report's guess of "1-3 proofs/s for 8-16 workers (low confidence)": | |
| **0.57/s on a heavy, unfiltered population and 4.4/s on a median population**; either way | |
| verification stays far above generation (~0.1-0.17 proofs/s per the report), so it is not the | |
| binding constraint at this worker count. | |
| ### 5.4 Test and mutation evidence | |
| * `pytest tests/test_lean_verifier.py -m "not slow"`: 173 passed (no Lean needed; fake REPL | |
| binary speaks the real protocol, including the audit message). | |
| * `pytest tests/test_lean_verifier.py -m slow`: 19 passed on the real toolchain (accepts a correct | |
| proof, rejects eight exploits with the lexical gate OFF, defeq-vs-strict mode, invalid reference, | |
| timeout + recovery, end-to-end reward path, concurrency, memory shape). | |
| * `tests/test_dependency_firewall.py`: 2 passed (`align/verify/lean/` and the two scripts import | |
| only stdlib / `align`). | |
| ## 6. Residual risks (what this verifier does NOT promise) | |
| 1. **Lexer desync.** The lexical gate is a hand-written tokenizer, not Lean's. It follows Lean's | |
| rules for nested comments, string escapes, raw strings, char literals and `«»`, and on any | |
| unusual construct falls back to a raw-text scan, but it has not been differentially fuzzed | |
| against Lean's real tokenizer. Mitigation: layers 3+ (axioms, bridge, kernel recheck, | |
| meta-declaration scan) do not use it; the semantic-only column of the ceiling test shows | |
| what they catch alone. What only the lexer stops is listed there (all of it is | |
| *policy*, not soundness: the proofs are valid for the stated theorem). | |
| 2. **No OS sandbox.** The REPL runs as the same user with filesystem access and no | |
| network namespace. Code can only execute inside the elaborator through meta-programming | |
| (`elab`/`macro`/`run_cmd`/`#eval`/`initialize`/`unsafe`), all of which are banned lexically | |
| AND detected by the meta-declaration scan, and the child's environment holds no secrets | |
| (`toolchain.build_child_env`). A meta-programming escape that evades both would run with the | |
| verifier's file permissions. For adversarial RL, run the pool as a dedicated unprivileged | |
| user or under `unshare --net` / a cgroup; that needs privileges this task did not have. | |
| 3. **Statement fidelity.** The bridge proves the candidate proves the GIVEN formal | |
| statement. It says nothing about whether that statement means what the informal problem | |
| says (miniF2F: formal/informal discrepancies in over half the problems, 16 unprovable | |
| statements; the report, section 5F). Report miniF2F-v2/PutnamBench with the pin and tier. | |
| 4. **Trust base.** The Lean kernel, `propext`, `Quot.sound`, `Classical.choice`, the REPL, and | |
| the prelude (`AUDIT_PRELUDE`, ~100 lines). The prelude is a trusted program; it is | |
| exercised by the real-toolchain tests, not formally verified. | |
| 5. **Policy rejections are not soundness failures.** `def`/`instance`/`notation`/`namespace` | |
| helpers, `exact?`, `#eval` and so on are rejected even when the proof of the stated theorem | |
| is valid. That is deliberate (smaller attack surface) and has a cost: public proofs that use | |
| them are false negatives. `allow_helper_defs=True` relaxes only `def`/`abbrev`. | |
| 6. **Version drift.** Proofs from other Lean/Mathlib versions will fail to compile (counted as | |
| 0), which is correct for this pin and wrong as a statement about the proof. | |
| 7. **Throughput numbers are for this population** (short Lean-Workbook-style and restated | |
| proofs). A prover that emits long `nlinarith`/`decide` proofs will be slower; the audit costs | |
| ~0.3 s for proofs whose dependency closure reaches real analysis and ~0.03 s for `omega`. | |
| ## 7. Amendments | |
| **7.1 Changes between the exploratory runs and the registration (before 20:12 IST; not | |
| amendments, listed so nothing is hidden).** Case-table edits made after seeing exploratory output: | |
| `sorry_branch` (its `all_goals sorry` was dead code, so the proof was valid: moved the `sorry` into | |
| a live branch); `sorry_after_char_quote` / `sorry_after_comment_opener_string` (did not compile; | |
| rebuilt so the `sorry` really sits between two quote-bearing literals); `axiom_private`, | |
| `axiom_reducible_attr` (did not compile; replaced by `axiom_polymorphic`, `axiom_via_simpa`); | |
| `ofReduceBool_term` (`rfl` cannot discharge `Lean.reduceBool`; replaced by | |
| `native_decide_in_helper_lemma`); `local_notation_hole` (a local `+` notation is *ambiguous* with | |
| the builtin one, so it never elaborates; replaced by `variable_hole_hypothesis`); | |
| `rs_default_arg` (did not compile). Verifier changes made before registration, each driven by a | |
| measurement: single import per process (Section 3), the late-answer drain, `kernel_recheck` | |
| default, the `open Lean`-root ban, `notation3`/`binder_predicate`/... bans, 180 s default budget, | |
| `split_imports`/prelude-before-opens layering. The `skipKernelTC_option` case turned out to be | |
| **inert on this pin** (the option does not exist) and was kept, labelled. | |
| **7.2 Retraction.** A first measurement reported the kernel recheck as costing "2.2-3.2x" | |
| (`lean_workbook_10009`: 21 s -> 68 s). That reading came from a run on a host where two other | |
| Lean processes were competing; isolated measurements (`#align_audit` flags 0 vs 1 on the same | |
| environment) show **+0.04 to +0.12 s** against 20-43 s of elaboration. The recheck is therefore ON | |
| by default; the claim was removed from the code comment before registration. (The same episode is | |
| why the headline timings here come from the registered run; the few ad-hoc timings are labelled | |
| exploratory.) | |
| **7.3 Verifier changes after registration:** none. The registered run used the tree that is | |
| described here. | |
| **7.4 Failures found by running against the real toolchain (each has a regression test).** | |
| (1) REPL never flushes a pipe -> `stdbuf`; (2) `lake env` paths are cwd-relative -> absolutised; | |
| (3) a second `import Mathlib` in a live REPL costs +3.9 GiB anonymous and tripped the 4 GiB | |
| watchdog on the first multi-case run -> one import per process, layered headers; (4) | |
| `#print axioms`'s collector is private in 4.9 -> own traversal; (5) with `autoImplicit` on (plain | |
| Lean default) an *unknown identifier* in a statement is silently an implicit variable, so "invalid | |
| reference statement" needs a genuine type error to trigger `reference_invalid`; (6) `sqrt` under | |
| `open Real Nat` is already ambiguous, so a root-level `def sqrt` is not a viable definition hole | |
| here (instances and `variable` are); (7) pool statistics were counted only when an error escaped | |
| `acquire`, not when the caller handled it. | |
| **7.5 Hand mutants** (each removes one fix in a COPY of the package, then runs the non-slow | |
| tests; `.tmp/lean_notes/mutants.py`, final run after the last code change except docs): | |
| | | Mutant (fix removed) | Killed by | | |
| |---|---|---| | |
| | M01 | accept any audit-tagged message (forgeable verdict) | `test_audit_message_without_the_nonce_is_ignored_forged_verdict_regression` | | |
| | M02 | no axiom whitelist check | `test_each_audit_finding_rejects[axiom/sorryax/ofreduce]` | | |
| | M03 | ignore the kernel statement bridge | `test_each_audit_finding_rejects[bridge]` | | |
| | M04 | no meta-baseline subtraction | `test_verdict_accepts_a_clean_audit` | | |
| | M05 | no meta-declaration check | `test_each_audit_finding_rejects[meta]` | | |
| | M06 | ignore `sorry` warnings / `sorries` | `test_each_audit_finding_rejects[sorrywarn/sorrylist]` | | |
| | M07 | header failure scored 0 instead of `LeanInfraError` | `test_header_failure_raises_infra_error_never_a_zero` | | |
| | M08 | candidate runs without the reference env | `test_command_order_and_envs_are_the_contract` | | |
| | M09 | no `stdbuf` | `test_wrapped_argv_uses_stdbuf_prlimit_and_nice` | | |
| | M10 | kill the pid, not the process group | `test_timeout_kills_grandchildren_too_via_process_group` | | |
| | M11 | no memory watchdog | `test_memory_watchdog_caps_anonymous_memory_not_rss` | | |
| | M12 | timeout without killing | `test_timeout_kills_the_process_and_leaves_no_zombie` | | |
| | M13 | tolerate bytes after the terminator | **survives: equivalent mutant** (two concatenated JSON objects fail `json.loads` anyway); the real defence is M39 | | |
| | M14 | first `:=` wins | `test_sig_end_skips_let_in_type_not_the_first_colon_eq` | | |
| | M15 | no char-literal handling | `test_lexer_char_literal_quote_is_not_a_string_start` | | |
| | M16 | block comments do not nest | `test_lexer_drops_line_and_nested_block_comments` | | |
| | M17 | no raw-scan fallback on lexer anomalies | `test_anomaly_triggers_raw_scan_...` | | |
| | M18 | re-import for every header variant | `test_second_header_variant_reuses_the_import_never_imports_twice_regression` | | |
| | M19 | lake paths left cwd-relative | `test_build_child_env_absolutises_...` | | |
| | M20 | no header equality | `test_static_rejections[header...]` | | |
| | M21 | `set_option` whitelist off | `test_banned_constructs_fire[set_option...]` | | |
| | M22 | dead pool emits zeros | `test_reward_raises_when_the_pool_looks_dead_...` | | |
| | M23 | defs accepted as proofs | `test_each_audit_finding_rejects[def]` | | |
| | M24 | kernel recheck never requested | `test_kernel_recheck_is_on_by_default_...` | | |
| | M25 | `skip_tc` ignored | `test_each_audit_finding_rejects[skiptc]` | | |
| | M26 | recheck failure ignored | `test_each_audit_finding_rejects[recheck]` | | |
| | M27 | private/attribute target allowed | `test_static_rejections[target_modifier]` | | |
| | M28 | signature token equality off | `test_static_rejections[statement_mismatch]` | | |
| | M29 | identifier ban list off | `test_banned_constructs_fire` | | |
| | M30 | salvage unclosed fences | `test_extract_unclosed_fence_is_not_salvaged` | | |
| | M31 | first fence instead of last | `test_extract_takes_last_closed_lean_fence_and_ignores_think` | | |
| | M32 | duplicate target allowed | `test_static_target_declared_twice_is_rejected` | | |
| | M33 | ablated verifier usable as a reward | `test_reward_refuses_the_ablated_verifier` | | |
| | M34 | worker never returned to the pool | **hang-killed** (the pool tests block; 90 s watchdog) | | |
| | M35 | no recycling after `max_requests` | `test_worker_recycled_after_max_requests` | | |
| | M36 | unbounded glibc arenas | `test_build_child_env_absolutises_...` | | |
| | M37 | child inherits the whole `lake env` | `test_build_child_env_absolutises_...` (secrets leak assertion) | | |
| | M38 | no output cap | `test_oversized_output_kills_the_process` | | |
| | M39 | late second answer attributed to the next request | `test_late_second_answer_is_detected_...` | | |
| | M40 | `open Lean` makes meta identifiers spellable | `test_banned_constructs_fire[open Lean...]` | | |
| 40 mutants: **38 killed by an assertion, 1 killed by a hang (M34), 1 survivor (M13, equivalent)**. | |
| Not covered by any mutant, by construction: the `skip_tc` audit path against the REAL option (it | |
| does not exist on this pin; `test_real_skip_kernel_tc_option_does_not_exist_on_the_pin` fails the | |
| day the pin changes) and the Lean prelude's kernel-recheck loop against a proof that elaborated | |
| with the kernel disabled (no way to produce one on 4.9 without the banned meta-programming). | |
| **7.2 V1 freeze (2026-10-14, branch `phase0/verifier`; PROPOSED FROZEN HASH, merge after chain6 exits).** | |
| Fixes (regressions in `tests/test_lean_hardening.py`, each adversarial proof was accepted or aborted a batch before): | |
| U08-1/2 lexer desync (`\/` and `//` are single tokens; a comment opener glued to any other symbol is refused as | |
| ambiguous; numerals no longer swallow letters: `1macro`; raw strings of any hash count; real-Lean differential fuzz); | |
| U08-40 `s!`/`f!`/`m!` interpolation flagged whatever sits before the quote; U08-4/12 `_align*` names and | |
| `_root_.`/`«»` spellings banned; U08-5 axiom-walk fuel exhaustion reports `_align_fuel_exhausted` (fails closed) and a | |
| missing `axioms` field is `audit_incomplete`; U08-3 `ReplPool.from_toolchain(sandbox=True)` is the default, fails closed | |
| with `SandboxUnavailable`, `sandbox=False` is the explicit opt-out; U08-8 every real REPL runs under the launcher/supervisor | |
| (`ReplProcess.supervise`) that SIGKILLs it when the parent process dies (measured: orphaned before, reaped now); | |
| U08-6 stale env ids after a worker death raise `ReplCrash`; U08-7 lone surrogates reject that item only; U08-17 budget | |
| starts after `acquire()`; U08-18 a bad reference header is `reference_invalid`, not a batch abort; U08-21 any exception | |
| mid-request kills the child; U08-11(codex) unmeasurable memory fails closed after `mem_miss_limit` samples; U08-16 | |
| timeout/crash/memory/protocol map to `Outcome.UNDECIDED` (new enum member in `align/eval/verifier.py`, reward stays 0). | |
| Ported from the red-team session: opt-in `independent_recheck` (`recheck.py`), default off. | |
| NOT changed (design choices, math verifier, need a decision): U08-9/10/11/15 (tolerance, texty fallback, unit stripping, | |
| `i`), U08-13/14/22 (extractor ReDoS/format), U08-19/20 (preflight opt-in, pin not enforced at runtime). | |
| Verification: exploratory ceiling run (2 workers, sandboxed): known-good 176/177 accepted (the one reject is a | |
| 179 s timeout under host load), adversarial 0/61 accepted. Registered run to do after the chains exit (needs the heavy | |
| lock): `python scripts/lean_ceiling_test.py --workers 8`. | |
| Frozen hash: `sha256` over, for each `align/verify/**/*.py` in sorted path order, `path + NUL + bytes + NUL`: | |
| `6df56348db12d8e255427764f994d0f3ab29d4713e84ddda07c9637c31f12648`. | |
| **7.3 FROZEN (2026-10-19).** Merged verifier (V1 + MV math-checker fixes) hash | |
| `f1dd9e7871ade3a9dd37587c10a7a17025fe8623a735455ec40206607e120b5c` (`scripts/p2/verifier_hash.py`; supersedes the | |
| 7.2 proposal, which predates MV). Registered ceiling run (8 workers, heavy lock, sandboxed, `.tmp/restart/ceiling/result.json`): | |
| known-good 178/179 accepted = 0.9944 (Wilson95 [0.9690, 0.9990], floor 0.98) PASS; adversarial 0/61 accepted PASS | |
| (a compile-only grader would accept 51). Every SOTA number from here on is verified under this hash; chain8 refuses to | |
| run a step if `align/verify` changes. | |
| ## 8. Operations | |
| ```bash | |
| bash scripts/lean_setup.sh # install / repair (idempotent, ~3 min, 5.8 GB, under /apps) | |
| bash scripts/lean_setup.sh --check # exit 0 iff the install is complete | |
| source /apps/development/rlhf/lean/env.sh # only if you want elan/lake in your shell | |
| pytest tests/test_lean_verifier.py -m "not slow" # no Lean needed (fake REPL binary) | |
| pytest tests/test_lean_verifier.py -m slow # real toolchain, 2 workers (~1 min) | |
| python scripts/lean_ceiling_test.py --verify-prereg | |
| python scripts/lean_ceiling_test.py --workers 8 # registered run: preflight + heavy-job lock | |
| python scripts/lean_ceiling_test.py --measure-only # install size, cold/warm import, PSS | |
| ``` | |
| * `ALIGN_LEAN_ROOT` relocates the install. Every cache the toolchain writes is redirected | |
| under it by `lean_setup.sh`; nothing goes to `$HOME`, `/` or `/tmp`. | |
| * Worker stderr spools to `<root>/tmp/pool/worker<i>.stderr` (truncated per respawn; the tail is | |
| quoted in `ReplCrash`). | |
| * Before widening the pool: `plan_workers` and `ReplPool.from_toolchain(preflight=True)` are the | |
| only sanctioned way; never construct more workers than the 45 GB floor allows. | |
| * A new Lean/Mathlib pin is a new verifier: re-run the ceiling test, update `PINS.json` | |
| provenance in Section 1, and regenerate any cached verdicts. | |
| * Hand mutants (Section 7) are run on a COPY of the package under `.tmp/mutants/`; the repo is | |
| never edited in place. | |