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. |
runtime:RuntimePackageMirror (D30) | 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. Naming: D28, D30 and this guide previously called it a LeanPackageMirror. No class and no Rust type of that name exists — the generator stamps urn:eigenius:runtime:RuntimePackageMirror, the substrate’s generic mirror class the Julia institutions also use, and the verifier reads that IRI. The old name still appears in some diagnostic strings the institution emits. |
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 an axiom allowlist, and confirms the named theorem is declared. The allowlist is a hard-coded four-element constant in the institution (DEFAULT_LEAN_AXIOMS), not a value read from the chain — see axiom allowlist policy below. Implementation:crates/eigenius-lean/src/checker.rs. Onecatch_unwindwrapscheck_all_declars, so a checker rejection — which nanoda reports by panicking — becomesVerdict::Failscarrying the panic message rather than a kernel crash. The parser call that precedes it sits outside the guard. -
Mirror correspondence. Two obligations, both in
crates/eigenius-lean/src/institution.rsunderdo_correspondence_check.check_mirror_covers_claim_classresolvesclaim_iriand requires the claim’s class IRI to appear in the mirror’smirrored_classeslist.check_proposition_structural_correspondencethen walks the proof term’sproposition— chain-mirrored aslean:LeanExpr— collects everyConstreference under theEigeniusFFInamespace, and requires at least one to map back, throughmirrored_classesand each class’score:short_name, to the claim’s class.Note what this does not do: it never compares the proposition against the proof bytes.
bytes_to_lean_expris authoring-side and is never called frominstitution.rs. Nothing establishes that the committed proposition is the type of the theorem nanoda checked, so a valid proof of one theorem bound to a claim about an unrelated class of the same mirror still yieldsHolds. Both anchors arerecommendsrather thanrequires, and each absent anchor silently skips its check. -
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 carrying a single diagnostic string. The institution defines exactly three prefixes for it — FFIVersionMismatch, AnchorContentHashMismatch, PropositionMismatch — written onto the front of the message so a consumer can match by leading token. D28 §9.1 enumerates more, AxiomNotPermitted among them; those were never built. An axiom violation arrives as an opaque nanoda panic string, because axiom policy is enforced inside the checker rather than by the institution.
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 [runtime:RuntimePackageMirror — 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 : { x : Float // 0.0 ≤ x }deriving Reprend EigeniusFFI-- Capstone.leanimport EigeniusFFItheorem patient_weight_nonneg : ∀ p : EigeniusFFI.Patient, 0.0 ≤ p.weight.val :=fun p => p.weight.propertyThat is the committed capstone verbatim; do not simplify it to a bare
weight : Floatprovedby omega, which is what this guide printed until 2026-08-20 and which does not compile —omegais a decision procedure for linear integer and natural arithmetic and does not apply toFloat. The refinement type is the point: D30 §9.1 lifts the chain-sidemin_value: 0.0constraint into Lean’s type system, so the nonnegativity obligation is discharged at construction and the theorem is the field’spropertyprojection. What the capstone demonstrates is the closed audit chain, not the proof’s difficulty. -
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 — the loadable five-resource Eigon document, and the fixture to read when you want the committed shapes. (The lean-verification.json this guide opens with is the notebook that queries the chain afterwards; it is not the fixture.) Use the generator as the executable spec.
Axiom allowlist policy
The allowlist the verifier uses is a hard-coded four-element constant, DEFAULT_LEAN_AXIOMS in crates/eigenius-lean/src/institution.rs: propext, Classical.choice, Quot.sound, Lean.trustCompiler — the community-baseline set of D28 §7.1.
Classical.choice stays in it because even trivial proofs through modern Lean stdlib pull it via Subtype’s projection helpers, so an empty allowlist would reject essentially every real proof.
The per-environment override D28 §12 describes has no effect on a verdict. lean:lean_permitted_axioms is declared, is requires on lean:LeanEnvironment, and is read by the authoring runtime; nothing on the verification path reads it. The institution’s own comment gives the reason — the wiring waits on the authoring runtime’s environment resource reaching the chain, and the environment IRI is not there today. A constructive-only deployment cannot express that policy by committing a tighter LeanEnvironment; changing the allowlist today is a code change.
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 Subtype.property projection against a refinement-typed Float field, 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 committedpropositionon theLeanProofTermreferences noEigeniusFFI.*type that maps back to the claim’s class. Nothing here compares the proposition against the proof bytes, so re-running the generator only helps if it changes which classes the proposition names. Check that the proposition mentions the mirrored type for the claim’s class, and that the class’score:short_namematches the Lean structure’s name.Verdict::FailswithFFIVersionMismatch— the mirror doesn’t resolve, itssource_layerisn’t reachable from the verification head, or the claim’s class isn’t inmirrored_classes. Check that the chain-side class declaration’s IRI appears inmirrored_classes. Note that check 2a comparessource_layeragainst the verification head’s ancestry rather than against the layer defining the claim’s class, so a class that acquired a required property after the proof was authored is not detected.Verdict::FailswithAnchorContentHashMismatch— the SHA-256 recomputed over the mirror’s embeddedlibrary_contentarchive doesn’t match the declaredlibrary_content_hash. The check compares two fields of one committed resource, so it catches an inconsistent pair, not a coherently rewritten mirror.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 — theruntime:RuntimePackageMirroris 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.