align / code /docs /LEAN_VERIFIER.md
viai957's picture
Add files using upload-large-folder tool
d909bd8 verified
|
Raw History Blame Contribute Delete
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 .oleans, 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, opens, 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:

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 .oleans 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 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.