Lean 4 institution tutorial
Slow-walk worked example of the platform’s first verification institution. Exercises the full audit-chain shape from D28 §5.7 end-to-end against a real Lean 4 proof: from the chain-committed Verdict::Holds back through the proof term, mirror anchor, and chain-side ontology class the proof discharges a claim about.
Read this if you want to know what’s actually happening when the lean-verification notebook runs, how to author your own proofs to land them on the chain, or how the in-process verification path stays tight (no Docker, no IPC, verdict as a direct function call).
Why Lean is different from the Julia institutions
The five Julia institutions follow the same shape: orchestrator-spawned worker containers hosting a Julia ecosystem, communicating with the kernel via Eigon-CBOR over UDS. The runtime substrate’s mirror create → env build → env create → institution install lifecycle drives every one.
Lean 4 splits this:
- Authoring side (substrate-hosted, just like Julia): writing Lean source, running
lake build, exporting vialean4export. Lives in aLeanEnvironmentDocker image with the toolchain pinned by digest. The substrate’sLeanLanguageRuntime(crates/eigenius-lean-runtime/) drives image builds and worker dispatch. - Verification side (in-process inside the kernel binary): re-checking
lean4exportbytes viananoda_lib. Lives incrates/eigenius-lean/, linked directly into the kernel’sclicrate. No IPC, no orchestrator round-trip.
The split is load-bearing for trust (D28 §2.3). The authoring side runs arbitrary user-provided Lean code in a container; the verification side runs nanoda_lib’s ~3 kLoC of audited Rust against the exported term. The verdict the kernel publishes is a direct function call inside the kernel process — no chance of a compromised worker forging a Holds.
This means a Lean institution is registered with runtime: in_process rather than runtime: external. The InProcess runtime kind (kernel/src/institution/registry.rs) lands in Phase 20a; future verification institutions (Rocq, Isabelle/HOL, SMT checkers) will follow the same factoring.
The five chain shapes
Verification leaves a typed audit trail with five resource kinds:
| Resource | Role |
|---|---|
LeanProject | A Lake project — lakefile.lean + lean-toolchain + source files. The substrate’s authoring runtime builds it and runs lean4export. |
LeanEnvironment | Digest-pinned Docker image + axiom allowlist + lockfile hash. Subclass of RuntimeEnvironment. The lockfile-hash field anchors the proof against a specific dependency-tree state. |
LeanPackageMirror | The generated EigeniusFFI Lake package that mirrors a chain layer’s classes into Lean structures. The proof’s proposition references types in this mirror; correspondence checking walks these. |
LeanProofPayload | Verbatim lean4export output as a string. The exact bytes nanoda re-checks; no compression, no canonicalisation. |
LeanProofTerm | Wires payload + mirror + claim together. Carries the chain-mirrored proposition (lean:LeanExpr) and the IRI of the chain-side claim the proof discharges. |
AutoOnLoad fires on LeanProofTerm commits and produces a sixth shape:
| Resource | Role |
|---|---|
Verdict (ctor_name: "Holds" / "Fails" / "Undecidable") | The institution’s outcome. Committed alongside a RuntimeInvocation provenance record. Queryable like any other chain resource. |
The three-part correspondence check
D28 §5.5 specifies what qc_proof_check verifies for every LeanProofTerm that commits. AutoOnLoad fires it; the kernel rejects the commit if it fails.
-
Proof validity.
nanoda_lib::check_proof(payload_bytes)parses the export JSON, type-checks each declaration against the axiom allowlist on theLeanEnvironment, and confirms the named theorem’s term has the named type. Implementation:crates/eigenius-lean/src/checker.rs. Panic-catching wraps the call so a bug in the bundled checker becomesVerdict::Failswith a structured diagnostic, not a kernel crash. -
Mirror correspondence. Walks the proof term’s proposition (chain-mirrored as
lean:LeanExpr) forConstreferences with names likeEigeniusFFI.Patient. Maps each back to a chain class IRI via theLeanPackageMirror’smirrored_classeslist. The chain claim’sis_a[0]must appear in that list. Implementation:crates/eigenius-lean/src/institution.rs’sstructural_correspondence_matches. -
Anchor consistency. Recomputes the SHA-256 over the mirror’s embedded
library_contentarchive (using the D30 §10.2 length-prefixed framing) and compares to the declaredlibrary_content_hash. Mismatch = tampering, even if the proof type-checks.
All three must pass for Verdict::Holds. Any one failing produces Verdict::Fails with a typed diagnostic (AxiomNotPermitted, PropositionMismatch, FFIVersionMismatch, etc.; see D28 §9.1).
Walking the audit chain (lean-verification notebook)
The capstone notebook (notebooks/examples/lean-verification.json) walks the cycle that closes between the verdict and the ontology class the proof is about. Read forward, the chain is:
Patient class [Class — the chain-side ontology root] ↑ is_apatient_1 [Patient — the Eigon claim instance] ↑ claim_iriproof_term [LeanProofTerm — proof + proposition + anchors] │ ↑ proof_payload │ proof_payload [LeanProofPayload — verbatim lean4export bytes] │ ↑ mirror_iri │ mirror [LeanPackageMirror — audit anchor] │ │ ↑ source_layer │ │ bootstrap head layer [universal ancestor of every claim layer] │ │ ↑ library_content_hash │ │ sha256:af662f…d6510e [tamper-detection anchor] │ │ ↑ library_content.files[] │ │ Capstone.lean, EigeniusFFI.lean, [the Lake project that produced the proof] │ │ lakefile.lean, lean-toolchain │ ↑ mirrored_classes │ Patient [back to the ontology root — cycle closes] ↑ verdict_subjectVerdict("Holds") [the verdict — D28 §5.7 closure]The notebook’s four EigenQL cells trace one backward edge each:
- Find the verdict.
MATCH "urn:eigenius:institution:Verdict"(?v) { "urn:eigenius:institution:verdict_subject": ?subj, "urn:eigenius:core:ctor_name": ?c } WHERE ?subj = "urn:eigenius:demo:lean:proof_term" RETURN [] { ... }— one row,ctor = "Holds". The IRI is the audit anchor for everything else. - Find the proof term. Match on
urn:eigenius:lean:LeanProofTerm, projecttarget_name,claim_iri,mirror_iri. Three cross-references that pin the proof to its chain-side counterparts. - Find the mirror. Match on
urn:eigenius:runtime:RuntimePackageMirror, projectsource_layer,library_content_hash,mirrored_classes. Thesource_layeris the IRI formurn:eigenius:layer:<hex>of the chain layer the mirrored classes were authored in. - Find the Patient class. Match on
urn:eigenius:core:Class, projectshort_name. Closes the loop —short_name = "Patient"is the string nanoda’s printer would renderEigeniusFFI.Patient’sConstreference as, and the structural correspondence check pinned this mapping.
Every byte that went into the verification — Lake project sources, the toolchain pin, the verbatim lean4export JSON, the chain-side class declaration, the source layer the mirror anchors to — sits on the chain as a typed, queryable, content-addressed resource. Nothing in the chain is trusted; everything is anchored.
Authoring your own proof
This is the high-level shape; the demo’s generator binary (crates/eigenius-lean/examples/gen_verification_demo.rs) is the reference implementation.
-
Author the chain side. Commit the class declaration and the instance the proof is about via standard ESL.
namespace core = "urn:eigenius:core";namespace ex = "urn:example";class ex:Patient {description = "...";}resource ex:patient_1 : ex:Patient { } -
Author the Lake project. A standalone Lake package that imports
EigeniusLeanCommon(the hand-authored helper package the substrate ships) and defines the structures the proof refers to undernamespace EigeniusFFI. The capstone proof atlean/research/capstone-proof/is the canonical reference shape.-- EigeniusFFI.leanimport EigeniusLeanCommonnamespace EigeniusFFIstructure Patient whereweight : Floatderiving Reprend EigeniusFFI-- Capstone.leanimport EigeniusFFItheorem patient_weight_nonneg :∀ p : EigeniusFFI.Patient, p.weight ≥ 0 → p.weight + 10 ≥ 10 := byintro p _; omega -
Build + export. Inside the Lake project:
Terminal window lake buildlake exe lean4export Capstone -- patient_weight_nonneg > proof.jsonThe
proof.jsonis what becomesLeanProofPayload.payload_bytes. It’s verbatim machine output; we don’t post-process it. -
Compute the library content hash. SHA-256 over the four files (
lakefile.lean,lean-toolchain,EigeniusFFI.lean,Capstone.lean) under D30 §10.2’s length-prefixed framing. Implementation: thelibrary_content_hashhelper that ships incrates/eigenius-lean-runtime/src/mirror_gen/module_assembler.rs. -
Decode the proposition.
bytes_to_lean_expr(proof_bytes, theorem_name)returns the proposition as aValue::Jsoncarrying alean:LeanExprvalue (D40 §3.3). The kernel’s validator type-checks this against the LeanExpr ontology at commit time; the institution’s structural correspondence check walks it forConstreferences. -
Commit the five resources (Patient class, instance, mirror, payload, proof term) as a single Eigon-JSON document loaded via
eigenius load. AutoOnLoad fires automatically on the proof term’s commit and produces the verdict resource.
The generator binary at crates/eigenius-lean/examples/gen_verification_demo.rs performs steps 2-6 for the capstone proof and writes the result to notebooks/examples/lean-verification-demo.eigon.json. Use it as the executable spec.
Axiom allowlist policy
Every LeanEnvironment declares a lean_permitted_axioms list (D28 §7.1). The institution’s default — and the value baked into the demo — is the four community-baseline axioms: propext, Classical.choice, Quot.sound, Lean.trustCompiler.
Classical.choice stays in the default because even trivial proofs through modern Lean stdlib pull it via Subtype’s projection helpers — an empty allowlist would reject essentially every real proof. Constructive-only deployments override per-env by stamping a tighter lean_permitted_axioms on their own LeanEnvironment resource. The override is the audited resource property, not a code patch (D28 §12 — resolved at end of Phase 20a).
Toolchain pin and upgrade
The pinned Lean toolchain lives at lean/runtime-worker/lean-toolchain — single source of truth. Three sibling pins (lean/common/EigeniusLeanCommon/lean-toolchain, lean/research/capstone-proof/lean-toolchain, lean/runtime-worker/vendor/lean4export/lean-toolchain) must match.
Upgrades are manual, not automatic — the substrate’s image-digest model makes every toolchain change a new content-addressed LeanEnvironment, so existing verified proofs stay valid against their original env digest. Auto-bump would fragment the env-digest space silently. The full upgrade checklist is at docs/notes/lean-toolchain-upgrade.md; follow it whenever bumping.
Lean toolchain prerequisite
The substrate’s Lake-driven worker (lean/runtime-worker/) depends on a Rust cdylib (libeigenius_lean_worker.so) built from crates/eigenius-lean-worker/. The crate is a regular workspace member, but its build.rs compiles a C bridge against Lean’s lean.h to talk to the Lean runtime — lean.h ships with the Lean toolchain via elan, not with system packages, so any host that builds the workspace needs elan + the pinned toolchain installed.
Install elan (one-time) per the standard Lean instructions:
curl https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh -sSf | shThe pinned toolchain version lives at lean/runtime-worker/lean-toolchain (single source of truth — same file the Dockerfile composer bakes into env images). Elan will install it automatically the first time anything reads that file, or you can pre-install:
elan toolchain install $(cat lean/runtime-worker/lean-toolchain)The build.rs (crates/eigenius-lean-worker/build.rs) discovers lean.h via, in order: EIGENIUS_LEAN_INCLUDE_DIR env var (explicit override), LEAN_SYSROOT env var (set by Lake during builds), or lean --print-prefix from PATH (the typical dev-host path).
CI installs elan in the workflow at .github/workflows/ci.yml so cargo clippy --workspace --all-targets and cargo test --workspace cleanly build everything including the worker cdylib.
The full e2e test (crates/eigenius-lean-runtime/tests/lean_image_build_e2e.rs) is #[ignore] because it also needs Docker + buildah for the env-image step; run with cargo test -p eigenius-lean-runtime --test lean_image_build_e2e -- --ignored when verifying changes to the substrate’s image-build pipeline.
Performance shape
A single-theorem proof of the capstone shape (one omega against Float ordering, transitively pulling in Subtype / Classical.choice / LE.le.toLT and the EigeniusFFI.Patient structure) exports to ~9 kLoC of JSON (~200 KB). nanoda_lib re-checks it in under a second on a modern laptop. The integration test crates/eigenius-lean/tests/capstone_test.rs is #[ignore] because of that cost; run with -- --ignored when verifying changes touching the institution.
Memory note: nanoda parses the full export into an Expr-tree before type-checking. For small proofs this is fast; for Mathlib-scale proofs the parsed tree can be hundreds of MB. The Phase 20b plan (implementation-plan.md §Phase 20b) sizes this up when a Mathlib-dependent consumer appears.
Troubleshooting
Verdict::FailswithPropositionMismatch— the proposition decoded from the proof bytes doesn’t match the chain-mirrored proposition on theLeanProofTerm. Almost always means the fixture was regenerated against different proof bytes; re-run the generator binary.Verdict::FailswithFFIVersionMismatch— the mirror’ssource_layerisn’t reachable from the claim’s layer, or the proposition references a class the mirror doesn’t list. Check that the chain-side class declaration’s IRI appears inmirrored_classes.FormatViolation: value '<hex>' does not match format 'urn:eigenius:core:formats:iri'— thesource_layervalue isn’t IRI-shaped. Wrap the hex layer ID inurn:eigenius:layer:<hex>. The institution’s check accepts both forms.MissingRequirederrors at commit — theLeanPackageMirroris missing a required property (short_name,description,language,generator_identifier, etc.). The generator binary’smirror_resource()function sets all of them; if you’re rolling your own, mirror its shape.- Verdict missing after kernel restart — fixed in Phase 20a final round. If recurring, check that the kernel ran a clean shutdown (
docker compose stop kernelwith sufficient grace period). The sync-write durability fix atstorage/rocksdb/src/lib.rsmakes branch-ref + layer commits durable across forced restarts.
Cross-references
- Platform §11 — Runtime substrate — the conceptual chapter the authoring side runs on top of.
- The formula language guide — sibling chain-mirroring story for FormulaTerm (D32). LeanExpr (D40) is the verification-institution counterpart.
- ESL §9 — Institutions in ESL — generic D14 institution surface; Lean uses the same dispatch shape as Julia institutions.
- D28 — Lean 4 as Verification Institution — the design spec for the institution itself.
- D30 — Eigon → Lean Faithful Translation — the mirror generator’s faithfulness spec.
- D40 — Chain-Mirrored Lean Expressions —
lean:LeanExpr/lean:LeanLevel/lean:LeanNameinductives. - Toolchain upgrade checklist — manual upgrade flow with regeneration steps.
- implementation-plan.md §Phase 20a — the phase plan for the institution, including the eight sub-milestones and the capstone test.