Record with Aeneas and Lean
Aeneas translates Rust, through Charon, into a functional model that can be
verified in Lean. cargo proofs records committed Lean proofs of that model,
automatically resolving Rust APIs and specification theorems from Aeneas metadata, or using explicit mappings.
This page covers what is specific to Aeneas. The common steps for signing in, previewing, and publishing apply as well. Requires Rust 1.91 or later and cargo-proofs 0.4.0 or later:
cargo install cargo-proofs --locked
1. Pin the registered Aeneas revision
The Aeneas tool page lists registered revisions. The initial
registered revision is
be86ab0f1989d04944292b345b24c435be3ad10a,
which uses Lean 4.35.0. Use the same Aeneas revision to generate the model and
for the Lean library dependency. Use its compatible Charon pin as described in
the upstream installation and extraction instructions.
For a Lean project in proofs/ next to the Rust package's Cargo.toml, put
leanprover/lean4:v4.35.0 in proofs/lean-toolchain. A minimal
proofs/lakefile.toml has:
name = "demo-proofs"
[[lean_lib]]
name = "Proofs"
[[require]]
name = "aeneas"
git = { url = "https://github.com/AeneasVerif/aeneas", subDir = "backends/lean" }
rev = "be86ab0f1989d04944292b345b24c435be3ad10a"
Run lake update in proofs/ to resolve and write lake-manifest.json.
Generate the model and write proofs using Aeneas's instructions. The example
on this page uses this layout; replace the names with your actual proof:
my-crate/
├── Cargo.toml
├── Cargo.lock
├── proofs.toml
├── src/lib.rs
└── proofs/
├── lean-toolchain
├── lakefile.toml
├── lake-manifest.json
├── Generated/ # Aeneas model and translation.json
├── Proofs.lean # root module: import Proofs.Increment
└── Proofs/
└── Increment.lean # theorem demo.increment_spec
Module names follow file paths: Proofs/Increment.lean is module
Proofs.Increment. With automatic selection from the Proofs entry module, the
root module Proofs.lean must import each proof module to be searched, for
example import Proofs.Increment. Custom Lake source directories are not
supported.
2. Create proofs.toml
From the Rust package directory (the one containing Cargo.toml):
cargo proofs init --tool aeneas --tool-version be86ab0f1989d04944292b345b24c435be3ad10a
The revision must be the full 40-character Aeneas commit in your Lake manifest. Review the generated report fields, then choose how specifications are selected: automatically from Aeneas metadata (next section), or with explicit mappings.
3. Select specifications automatically
Generate the committed model and mapping together, using the pinned translator:
charon cargo --preset=aeneas --dest-file model.llbc -- --lib --locked
aeneas -backend lean -emit-json -dest proofs/Generated model.llbc
Then edit the [tool] section of proofs.toml to name the proof entry modules:
[tool]
name = "aeneas"
version = "be86ab0f1989d04944292b345b24c435be3ad10a"
lean_dir = "proofs"
proof_modules = ["Proofs"]
translation_manifest = "Generated/translation.json"
The manifest path is relative to lean_dir and defaults to translation.json.
Commit it with the generated Lean, Rust and proofs. Entry modules and their
transitive imports are searched only where their sources belong to this Lake
package. Dependency packages such as Aeneas and Mathlib are excluded. Ordinary
helper theorems are ignored; recognized specifications with unresolved public
APIs produce diagnostics. Loop helpers are never treated as independent APIs.
If an API has several specification theorems, each becomes a separate claim with
its own assumptions, conclusion and recorded evidence. A theorem that applies
only to a specific argument remains separate from a universally quantified theorem.
Zero selected specifications is an error. There is no discover subcommand.
Because proof_modules = ["Proofs"] names the root module, every module that
Proofs.lean imports, directly or transitively, is searched.
Explicit selection and existing projects
Instead of, or in addition to, automatic resolution, edit the [tool] section
of the proofs.toml created in step 2 to map theorems to APIs explicitly:
[tool]
name = "aeneas"
version = "be86ab0f1989d04944292b345b24c435be3ad10a"
lean_dir = "proofs"
[[tool.proofs]]
api = "increment"
module = "Proofs.Increment"
theorem = "demo.increment_spec"
precondition = "x < u32::MAX"
api is a public Rust callable path relative to the library. precondition is
optional author prose; existing text is retained. Empty prose means readers must
consult the proposition, including input types and type class instances. It does
not mean “no conditions.” Explicit mappings take precedence over automatic
resolution, and may select a theorem outside Aeneas's standard specification
shape. A manually selected theorem is never automatically assigned another API.
Projects without a translation manifest can continue to use only these mappings.
Each selected theorem maps to one public Rust callable. Multiple [[tool.proofs]]
entries may name the same API with different theorems. A manual mapping overrides
that theorem's automatic mapping; other specifications for the API are still included.
lean_dir is relative to Cargo.toml and defaults to ".". For how Claim IDs
are kept when theorems are added, removed, or renamed, see
Aeneas evidence details.
4. Commit, record, preview, and publish
Commit and push Rust source, Cargo.lock, proofs.toml, the generated Lean
model and proof sources, lakefile.toml (or lakefile.lean),
lake-manifest.json, and lean-toolchain together. Ignore .lake/ and Lean
compiled artifacts such as .olean and .ilean; the recorder builds the
mapped modules from source in a detached checkout.
From the Lean project directory:
cd proofs
cargo proofs run --manifest-path ../Cargo.toml -- lake build
cargo proofs preview --manifest-path ../Cargo.toml
cargo proofs publish --manifest-path ../Cargo.toml --dry-run
cargo proofs publish --manifest-path ../Cargo.toml
The preview command requires Node.js 24 or newer. Review the report and each local claim page, then stop the server with Ctrl+C before continuing. It reads the recorded run without running Lean again. See Preview a report locally for editing prose, selecting another run, and interpreting warnings.
Sign in with cargo proofs login before publishing if needed. The tool revision
must be registered and selectable on the service. Changes to mappings or
preconditions require another recording.
What a recorded result establishes
The recorder builds every mapped module explicitly, then checks that each
mapped name is a Lean theorem. The transitive axiom audit allows only propext,
Classical.choice, and Quot.sound. It rejects sorryAx, custom axioms
(including axiomatized external models), missing theorems, and failed builds.
The recorded SARIF stores each theorem's full proposition, which Claim pages show
as a Readable Lean view; see Aeneas evidence details for
the recorded evidence format.
The checked result is the named Lean theorem under the allowed axioms. Charon/Aeneas extraction is not rerun by the recorder. In the report, explain why the model matches the Rust commit and any additional trusted boundaries. The manifest is translation provenance, not evidence that translation was rerun or that the committed Rust, manifest and Lean are synchronized. Review the specification's strength as well as the proof.
The current adapter supports the default Cargo configuration, ordinary module
paths (Proofs/Increment.lean for Proofs.Increment), and the official pinned
Aeneas Git dependency. It does not support custom Lake source directories,
local-path Aeneas dependencies, feature/target-specific extraction, or the Coq,
F*, and HOL4 backends. See the version's limitations
and CLI documentation.
Use a cumulative Aeneas patch
A registered official base commit + one committed cumulative patch can replace
an official Git dependency. No public fork or experimental branch is required.
This supports the OCaml translator and backends/lean models, lemmas and tactics.
The Charon pin, Lean release, aeneas package name and Lake dependency configuration
of the base must remain unchanged. Other tools do not yet support patches.
First commit your changes in a separate local Aeneas checkout. Save its complete committed difference into the proof repository (new files must first be committed):
# In the modified Aeneas checkout; BASE_COMMIT is the registered full 40-character SHA.
git diff --binary --full-index --no-ext-diff --no-textconv BASE_COMMIT HEAD -- > /path/to/proof-repo/proofs/toolchain/aeneas.patch
Add this to the existing [tool] section of proofs.toml:
patch = "proofs/toolchain/aeneas.patch"
The patch path is relative to the selected Cargo package, where proofs.toml
lives. Add .proofs-toolchain/ to proofs/.gitignore, then run:
cargo proofs prepare --manifest-path Cargo.toml
Prepare works before the first Lake update and before committing or pushing. It
restores .proofs-toolchain/aeneas/ inside lean_dir. It does not edit tracked
Lake configuration, execute proofs, translate Rust, create a Run, or publish.
It refuses unmanaged destinations and edited managed sources; move those aside
before preparing again if you want to preserve local edits.
Replace the Aeneas require entry in proofs/lakefile.toml:
[[require]]
name = "aeneas"
path = ".proofs-toolchain/aeneas/backends/lean"
Then complete the first record:
cd proofs
lake update
lake build
# Commit and push proofs.toml, the patch, sources, lean-toolchain,
# lakefile and lake-manifest.json, plus Cargo.lock. Exclude .lake/ and .proofs-toolchain/.
cargo proofs run --manifest-path ../Cargo.toml -- lake build
cargo proofs preview --manifest-path ../Cargo.toml
cargo proofs publish --manifest-path ../Cargo.toml
For a patch update, regenerate the single cumulative diff against the same
registered base, run prepare and Lake update/build again, commit/push the inputs,
and record a new Run. Changed bytes at the same patch path require a new Run;
--force cannot override this. Description-only edits can reuse the previous Run.
Prepare and run both support the normal --manifest-path and -p selectors;
run must execute from the configured Lean directory.
Record always restores from the published proof commit into a new isolated
checkout. It builds the patched Lean library from source with Lake cache downloads
disabled, confirms Lake's effective dependency and selected Lean version, extracts
theorems, and audits transitive axioms (only propext, Classical.choice and
Quot.sound). It checks source content, file modes and source additions/deletions
afterward. It does not build the OCaml translator or run Charon/Rust → Lean.
translation_rerun remains false. Restoring a translator source does not establish
which translator generated the committed model.
Run metadata records provenance once in properties.proofs.toolchain:
schema_version = 1, a components map and role-specific uses. Aeneas's
lean_library and translator roles share the same source component; Charon is
an official pinned Git source and Lean an official release. Only Aeneas is
materialized. Uses record dependency resolution, proof-checker execution, and
unexecuted translator/frontend stages. These observations are facts about this
Run, not proof grades or binary authentication.
Three comparisons are separate: the input fingerprint includes paths, configuration and source bytes for local Run reuse; source identity includes tool/repository/base/patch digest (not patch path or component ID); observations include the restored tree and actual version output. Report revisions containing patched Runs require the same toolchain source identities, and equal identities must have equal restored trees. Patched/legacy mixtures are rejected. The original Run remains immutable. Its stored patch link is fixed to the recorded proof commit.
Preview/publish compare current inputs without fetching or rebuilding; deleting the managed directory does not invalidate matching evidence. Reproduce prepares the recorded package before building and auditing Lean. Existing Runs without metadata remain usable and show provenance as not recorded. Base tool limitations describe the official base. The service validates submitted records; it does not independently reproduce the author's execution or review the patch.
Independently patched Charon
Use tool.charon_patch alongside or instead of tool.patch; see the
configuration guide. The Charon base comes from
the official Aeneas charon-pin. Prepare restores patched sources but does not
run Rust-to-Lean extraction. Commit Rust sources, generated models and proofs so
an isolated checkout can build them without repository-specific setup hooks.
When tool.expand_api is enabled, API enumeration also includes inherited default
methods from active traits defined in the selected crate. Explicit implementations
replace those defaults. Private traits and unknown external trait defaults are not
exposed. This enumerates callable paths only and supplies no proof of their behavior.