This repository runs public Lean proof checks on a fresh GitHub-hosted VM. A maintainer starts Verify a Lean proof from the Actions tab. The workflow uploads full logs and a JSON receipt, including failed and incomplete checks. No leaderboard file is changed by a run.
The workflow has five inputs:
| Input | Meaning |
|---|---|
repo_url |
Public https://github.com/OWNER/REPO submission URL |
commit |
Complete 40-digit commit SHA; branch names are rejected |
challenge_file |
Registered statement file in this verifier repository |
theorem_names |
Comma-separated names allowed by that statement's profile |
lean_toolchain |
Toolchain pinned by the reviewed profile |
The first profile is challenges/dft/UniformFourier233.lean, Lean 4.34.1.
It checks OAI.PowerSaving.transform_mainY and
OAI.PowerSaving.convolution_mainY at saving 7547360/10^10.
The dispatch defaults pin Chafik Boukhalfa's (chafreaky) PR #2
head 837dc3130adc79f7b7ab4ddd56c15576bbd766b0, observed 2026-10-10.
A new challenge or toolchain requires a reviewed repository change.
- Fetch the pinned submission without running its scripts or Lake file.
- Build pinned Comparator, exporter, nanoda, and landrun tools. Fetch the pinned Mathlib cache in a separate trusted setup step.
- Copy regular Lean source files into a fresh project with our Lake config. Comparator first builds and exports our trusted challenge. Its solution build then compiles dependency chains from source, one new module at a time.
- Compare the solution statement and its dependencies with our challenge, check permitted axioms, and replay the proof in Lean's kernel.
- Run
#print axiomsfor each target. Run Comparator again with nanoda enabled; that pass also repeats Lean's kernel check. - Upload
receipt.json, source/tool hashes, per-stage logs, times, and sampled cgroup memory peaks asverifier-receipt-and-logs-….
The receipt reports each theorem and kernel separately. The theorems in one request share a proof closure, so their time and memory measurements are shared. A complete pass requires both kernels and the axiom audit. A failed workflow can still contain a completed Lean-kernel pass; read the per-kernel receipt. A timeout or resource limit gives no mathematical verdict.
The trusted DFT statement is based on our saved OpenAI challenge, not the submission's challenge. It covers every positive length and every complex input in the same exact scalar/index RAM, including root-order selection, supplied canonical root, preparation cost, and polynomial integer/work bounds. It is an operation-count theorem, not a bit-complexity claim.
permissions: contents: read; checkout does not retain credentials. The workflow uses no repository secrets and no Actions cache upload/save action.- A new VM runs each job. Network access is used only for public source fetches
and trusted tool/dependency setup. No submission code runs during setup.
Setup loads Ubuntu's packaged
bwrap-userns-restrictAppArmor profile so bubblewrap can create its namespaces; global userns restrictions stay enabled. - Submission compilation, export, kernel checks, and audit run in a bwrap namespace with no network, a private process namespace, and an empty environment except for explicit tool paths. Comparator also retains landrun and its AF_UNIX restriction. Missing isolation causes a failure.
- Only public system executables/libraries, the toolchain/tools, the fixed dependency cache, staged sources, and trusted control files are readable. Only the run's build directory and temporary directory are writable. The workflow checkout, credentials, host process tree, receipts, and cache are outside the submission's writable namespace.
- Submission Lake files, hooks, generators, submodules and compiled objects are not used. The trusted challenge imports only fixed Mathlib. Its compiled outputs are removed inside the sandbox before each Comparator call so an earlier submission build cannot replace the statement used for comparison.
- One workflow runs at a time. New modules compile in dependency order using four Lean threads, nice 10, a 4-CPU quota and a 14 GiB RAM cgroup maximum. Builds and the first Lean check have no swap. The nanoda phase gets 16 GiB of swap on the disposable VM because Comparator retains its parsed proof while the external kernel runs. The swap file is outside the submission mounts; receipts record sampled swap use separately. This uses the standard free public runner. There is no soft memory limit that would force prolonged reclaim.
- The job timeout is 350 minutes. Verification stops at a 320-minute total deadline, leaving time to save artifacts. Tool setup has a 45-minute limit. The controller also stops a check if free disk falls below 1 GiB.
- Setup removes only known preinstalled SDK folders on the disposable runner to make room for Lean and Mathlib. It does not reuse proof objects or publish cache entries from an untrusted build.
A profile with "runner": "github-split" runs in two hosted jobs of one
workflow run. Today only catalan has it. Catalan opened on 2026-10-27, after
the maintainer smoke below passed (run 40497685416, issue #68). Each job has the 350-minute job timeout and its
own 320-minute deadline from its own prepare. The PR route has no build job,
so a PR for a split profile starts no job.
- Job
build(contents: read,issues: read, no secrets) runs the same gate, tests,prepareandsetup. Thenverifier.py buildruns one parallellake build <solution module>with the real Lake (not the chain shim) in the same bwrap sandbox, in the unitbos-verifier-build. The unit has the same caps as the checks and no swap. It stops 15 minutes before the deadline. A failed or partial build does not stop the job. The receipt records the exit code, the unit result and the sampled peak memory. verifier.py pack(trusted, rule S2) runs after the unit has stopped. It walks.lake/buildwithlstatand never follows a symlink. It keeps only the allow-list below (regular files with one link, and directories) and records the count and the first 20 names of the other paths. It refuses more than 8 GiB or 200,000 members. It writes a sorted tar with fixed owners and modes and the real mtimes, to the nanosecond. The job uploads that tar, never the raw folder, asverifier-build-<run>-<attempt>-<issue>(1 day). Its sha256 is the job outputbuild_sha256. That hash covers onlybuild.tar.build.jsonandbuild.logare data from a trusted step of the same run (only that run's jobs can upload to it), and they go to the receipt as data only.- Job
verifyhasneeds: build. Its gate also stops (claimed only) when job 1's gate saidfalse. Itsif:uses!cancelled(), because GitHub skips a job whose needed job was skipped unless a status function is used. So a run without split works as before. Aftersetup, a split run downloads the tar and runsverifier.py unpack(trusted, rule S1). The build job must have succeeded, andBOS_BUILD_SHA256must equal the sha256 of the tar. Unpack checks every member before it writes a file. Only regular files and directories with relative UTF-8 names in the allow-list are permitted. One other member refuses the whole archive. A source root may not have the name of an entry in the toolchain'slib/lean. Unpack writes into the empty build folder fromprepare, withO_EXCLandO_NOFOLLOW, and sets the mtimes. If unpack fails, no check runs, and the receipt ends assetup_failed. The report job runs as before. preparewrites one chain target for a split profile, the solution module. So Comparator's solution build is one Lake call through the shim. It finds the restored files up to date, or rebuilds at the submission's own time cost. The kernel checks, the audit and the nanoda pass do not change. Nanoda gets the time that is left; for Catalan we expectincomplete: total_deadline.
The allow-list (verifier.build_allowed, one function for pack and unpack),
for each source root R of the profile and its solution dependencies:
lib/lean/R.<ext>, lib/lean/R/…, ir/R.<ext> and ir/R/…. Nothing else:
no VerifierChallenge*, no VerifierAudit*, no Mathlib/…, no other folder.
A source root cannot be Init, Std, Lean, Lake or Verifier*.
In one job, Comparator builds and exports the challenge before submission code runs. In job 2, the restored files are there before the challenge build. Lake source at Lean v4.34.1 and v4.33.1 (the same lines in both tags):
src/lake/Lake/Config/Workspace.lean:356-357:leanPathispackages.foldl (fun dirs pkg => pkg.leanLibDir :: dirs) []. The root package ispackages[0](line 105;Load/Workspace.lean:37makes it with index 0). So the root's.lake/build/lib/leancomes after every dependency package's lib folder.Workspace.lean:391-392and:422:lake envsetsLEAN_PATHtoleanPath ++ lakeEnv.leanPath.Config/Env.lean:255puts Lake'slibDirfirst inlakeEnv.leanPath. For the toolchain's own Lake,Config/InstallPath.lean:195-199(LakeInstall.ofLean, used at :423-427) sets it to the toolchain'slib/lean. So the root's lib folder comes before the toolchain's lib folder (Init,Std,Lean,Lake).src/Lean/Util/Path.lean:61-64(SearchPath.findWithExt) takes the first entry that has a folder or.oleanfile named after the first component of the module name.findOLean(line 117) uses it and does not check that the module file exists.Path.lean:102and:109putLEAN_PATHbefore the built-in toolchain path.
So files in the root build folder cannot shadow Mathlib or another dependency
package. They can shadow the toolchain's own modules: a lib/lean/Init/ folder
there would replace Init for every later Lean process. Rule S1 is the reason
that the split is safe: job 2 restores only the solution's own roots, and a
root cannot be Init, Std, Lean, Lake or Verifier*. The challenge
imports only Mathlib. Before it restores a file, unpack runs lake env printenv LEAN_PATH in the sandbox (our Lake config only, no submission code) and saves
the result as lean_path in the receipt, so each split run records the order.
Shared libraries: lake env also sets LD_LIBRARY_PATH
(Workspace.lean:372-373, :409-410, :431; Package.lean:374-375;
InstallPath.lean:160-164). The order is the toolchain's lib/lean and lib,
then each package's .lake/build/lib (the root first), then the initial value.
The root folder comes after the toolchain, but the Linux loader reads
LD_LIBRARY_PATH before /etc/ld.so.cache and the system folders. So a
library that the toolchain does not ship, for example libc.so.6, in the root
.lake/build/lib would load in each later process that lake env starts. S1
also keeps shared libraries out of those folders: it restores nothing directly
in lib/ or lib/lean/.
Note on the existing single-job path (not changed here): the nanoda pass and
the axiom audit start after the submission build of the same job. So their
processes can load a planted library or a shadowed Init. The Lean pass verdict
is not affected: Comparator exports the challenge before the solution build,
and it checks the axioms and replays the kernel in its own process, which
started before the build. The same applies to job 2 when its one Lake call
rebuilds a module.
A test_only artifact makes the site exclude the claim of its issue, so the
smoke must never run on another person's issue. A maintainer opens a test
issue with the form of smoke.CATALAN_FIXTURE: problem catalan, the fixture
repository URL, commit a3d48494df743e8dbf2dd5acb70dbe27759069e0, bound
1091, and the label test (a label needs triage rights). Then dispatch
Verify direct submissions with smoke_route: issue and that issue number.
Dispatch refuses an issue without the label, and an issue whose author is not
the user who dispatches the run (GITHUB_ACTOR). It does not check the author
association: the job token sees a private organization member as
CONTRIBUTOR (issue #68, 2026-10-27). The acknowledgement pass
and the scan skip issues with the label test, so only the dispatch runs a test
issue. The run is test_only and never imported. It posts no result comment,
no Lean checkpoint comment and no label. Read the artifacts.
The smoke of 2026-10-27 (verifier 6032768): job 1 built in 6178 s with a peak of
9.79 GB and packed 23,792 files (2.94 GB, 0 skipped). In job 2, the Lean kernel
and the axiom audit ended 142 minutes after prepare, so 178 minutes of the
320-minute deadline were left for nanoda.
- The Lean checkpoint watcher (
live_publish.watch, 330 minutes) cannot see the Lean checkpoint of a split run, which comes about 6-7 hours after the start. So only the final report posts. - The queue lease is held for the whole split run:
queue_lease.acquirehas no time limit, and the run takes up to about 2 x 350 minutes. - Nanoda usually ends as
incomplete(total_deadlineortimeout): Comparator replays the proof again in the nanoda pass. This does not prevent Lean Verified.
GitHub's runner reference
lists public ubuntu-latest as 4 vCPU, 16 GB RAM and 14 GB SSD, checked
2026-10-10. These are the documented standard-runner specifications; receipts
record the run's measured usage. A proof can exceed those resources.
OpenAI wrote the exact RAM formalization and all-length DFT reduction at
openai/math commit fd4aeeb2ee4fc729c18d98444fed42fd0529eeeb.
Our challenge changes seven names with suffix Y and changes the decimal
saving. All machine definitions and quantifiers remain unchanged. See NOTICE.
The submitted proof builds on Jacob Sussman's checker and framework and the community circuit credited in PR #2. Authorship of that proof stays with its authors. Comparator, Lean, nanoda, landrun, bwrap, the OS, and the pinned Mathlib cache are part of the checking environment. Tool and source hashes are included for audit. This runner does not perform line-by-line human review.
Logs can contain untrusted submission output. The controller does not stream that output as Actions workflow commands. The authoritative gate is the exit status of the pinned Comparator plus its completed acceptance output.
Local controller tests require only Python:
python3 -m unittest discover -s tests -vLean compilation runs in GitHub Actions, not on the office host.