Skip to content

2026

hax 0.4: easier to install and configure, and a new Lean backend

We are happy to announce hax 0.4. The release brings a one-command installation for the Lean backend, proof scenarios configured in a hax.toml file, a new Lean backend built on Aeneas, and new capabilities for the specification macros. This post walks through these highlights. The changelog lists everything else, including many fixes and improvements to the engine, the proof libraries, and the core models.

Installing hax

Until now, installing hax meant setting up an OCaml toolchain for the hax engine or using Nix or Docker. The new Lean backend does not use the hax engine. Instead, the cargo-hax command line tool drives Charon and Aeneas, which translate Rust into Lean. Installing cargo-hax and a Lean toolchain is therefore enough, given a C compiler and rustup, which Charon uses at extraction time. hax 0.4 publishes cargo-hax as a pre-built binary for Linux (x86_64 and aarch64) and macOS (aarch64). The binary needs glibc 2.35 or newer on Linux, and macOS 11 or newer. With cargo-binstall available, one command installs it:

cargo binstall cargo-hax

cargo install --locked cargo-hax builds the same binary from crates.io and covers systems the pre-built binary does not support. Charon and Aeneas need no manual installation. hax downloads them on first use, verifies them against a manifest shipped with the release, and caches them.

cargo-hax and hax-lib are released together and must match. A 0.4.2 binary accepts only hax-lib 0.4.2 and otherwise aborts with instructions on how to resolve the mismatch.

The F*, Rocq, ProVerif, and other backends still run through the hax engine and need the full installation via the setup script, Nix, or Docker, as described in the README.

Configuring a project

Before 0.4, a cargo hax invocation carried its configuration as flags: item selection, backend options, extra tool arguments. With 0.4, a project stores this configuration as proof scenarios in a hax.toml next to its Cargo.toml. The barrett example's hax.toml declares two scenarios, one for Lean and one for F*:

[scenario.barrett]
backend = "lean"

[scenario.barrett-fstar]
backend = "fstar"
z3rlimit = 100

cargo hax extract runs every scenario of the project, and cargo hax extract barrett runs a single scenario. Each scenario writes to proofs/<scenario>/<backend>/, so several extractions of one crate coexist. Scenarios support item selection (currently for the Lean backend only), cargo features, environment variables, and backend-specific options. The manual documents the keys.

Charon, Aeneas, the Lean toolchain, and the hax Lean library have default versions shipped and tested with each hax release. Most projects should use these defaults. If needed, hax.toml can override them: [tools] pins Charon and Aeneas, while [versions] pins the Lean toolchain and the hax Lean library, which models Rust core. This is an advanced option: other combinations are untested and are your responsibility to maintain across hax releases.

The new cargo hax tools subcommands let you inspect the versions in use, install them ahead of time for CI or offline work, and clean the tool cache.

The hax version itself can be pinned alongside the hax-lib version in Cargo.toml with cargo-run-bin:

[package.metadata.bin]
cargo-hax = { version = "0.4.2", bins = ["cargo-hax"], locked = true }

cargo-run-bin installs the pinned version on demand, and after cargo bin --sync-aliases, the usual cargo hax invocation uses that version.

Finally, cargo hax now exits with a non-zero status when an extraction fails. This lets a scenario run serve as a CI check, but CI scripts that relied on a zero exit status need adjusting. cargo hax extract followed by git diff --exit-code detects when the committed extraction output is out of date.

The Lean/Aeneas backend

In the new Lean backend, Charon compiles Rust into an intermediate representation, and Aeneas translates it into pure functional Lean code. On top of this pipeline, hax extracts hax_lib pre- and postconditions as Lean specifications, provides the hax Lean library, and generates a complete Lean package that builds with lake build. The previous Lean backend, based on the hax engine, remains available as legacy-lean.

Take a slightly simplified version of the Barrett reduction from the barrett example:

#[hax_lib::requires((i64::from(value) >= -BARRETT_R && i64::from(value) <= BARRETT_R))]
#[hax_lib::ensures(|result| result > -FIELD_MODULUS && result < FIELD_MODULUS &&
                       result.rem_euclid(FIELD_MODULUS) == value.rem_euclid(FIELD_MODULUS))]
pub fn barrett_reduce(value: FieldElement) -> FieldElement {
    let t = i64::from(value) * BARRETT_MULTIPLIER;
    let t = t + (BARRETT_R >> 1);
    let quotient = t >> BARRETT_SHIFT;
    let quotient = quotient as i32;
    let sub = quotient * FIELD_MODULUS;
    value - sub
}

Running the barrett scenario turns the function into a definition in the RustM monad, which models failures such as panics and integer overflow. The requires and ensures attributes become barrett_reduce.pre and barrett_reduce.post, and hax combines them into a specification:

def barrett_reduce.spec (value : Std.I32) : Prop :=
  (barrett_reduce.pre value).holds →
  ⦃ ⌜ True ⌝ ⦄
  barrett_reduce value
  ⦃ ⇓ res => ⌜ (barrett_reduce.post value res).holds ⌝ ⦄

The ⦃ ... ⦄ brackets enclose a Hoare triple around the call, with a postcondition on the result res. The triple itself has the trivial precondition True, and the actual precondition is the implication in front of it. The specification states that, if the precondition holds, the function returns without failing and its result satisfies the postcondition.

hax also generates a theorem for this specification, whose proof is sorry, Lean's placeholder for a missing proof. The theorem lives in a Verification folder that hax never overwrites. The barrett example contains the finished proof, which uses the hax_mvcgen tactic and grind. The Lean quick start shows the workflow, the Lean tutorial walks through proving panic freedom and functional properties, and the examples in the repository provide larger cases.

Specification updates

hax_lib::ensures takes the return value by value, which does not work for non-Copy types. hax 0.4 adds hax_lib::ensures_ref, which takes it by reference:

#[hax_lib::ensures_ref(|result| result.len() == 2)]
pub fn pair(x: u64) -> Vec<u64> {
    vec![x, x]
}

requires and ensures now also work under cfg_attr in impl blocks and on methods whose signatures mention associated types. Cases that are still unsupported produce an explicit error instead of silently dropping the specification.

hax can now extract specifications written with anodized, a crate that aims to provide one specification syntax for several Rust verification tools. hax extracts anodized's #[spec(requires: .., maintains: .., ensures: ..)] attributes as pre- and postconditions, where a maintains clause becomes both. hax_lib::requires and hax_lib::ensures remain the recommended way to write specifications for hax. The anodized example shows both side by side.

Thanks and what's next

Thanks to everyone who contributed to this release:

Our main goal for the next release is robustness. We want hax to work on almost any crate with all backends. We will expand our test suite to track progress towards this goal. We also plan to improve proof ergonomics, making it easier to go from an extraction to a finished proof.

As always, bug reports and questions are welcome on the issue tracker and on Zulip.

This Month in Hax: August 2026

In August, we successfully merged 46 pull requests!

We extended our CLI functionality significantly: hax can now be installed via cargo binstall cargo-hax, and it can automatically download and manage its aeenas and charon dependencies. Moreover, users can declare proof scenarios in a hax.toml file to specify exactly which options they want to use for a particular project, and then run those scenarios via a new cargo hax extract command.

We extended our models of Rust core further and improved its testing infrastructure to make sure that it is an accurate model of Rust core. These models benefit both the Lean and the F* backend.

For the new Lean backend, we added support for hax_lib::requires, hax_lib::ensures, and other attributes and for the hax_lib::prop and hax_lib::int specification helpers.

This was our final sprint towards a release candidate for hax v0.4.0. The release candidate was published yesterday. Now, we are testing it, fixing bugs, and finalizing its documentation.

Thanks to the many external contributors we had this month!

Full list of PRs

Contributors

This Month in Hax: July 2026

In July, we successfully merged 31 pull requests!

We are preparing for the release of hax 0.4.0. The main change is that the Lean backend now uses aeneas, the Lean backend relying on the hax engine has been renamed legacy-lean. The integration with aeneas has been improved and is still one of our main focuses.

This includes improvements and fixes to the core-models library. The rust models have been reintegrated in the hax repo, and now extract to both Lean, through the new Lean backend, and F*. We also fixed issues with the hax-lib annotation macros.

Finally, we worked on the documentation and updating the examples to show the latest workflow with our main backends (Lean and F*).

Full list of PRs

Contributors

This Month in Hax: June 2026

In June, we successfully merged 6 pull requests!

Our main focus right now is the integration of aeneas into hax, which we will release in July. Besides that, we worked on improving our testing infrastructure and on our Rust core models.

Full list of PRs

Contributors

This Month in Hax: May 2026

In May, we successfully merged 6 pull requests!

We fixed various bugs and finally released hax 0.3.7! Our work to integrate Aeneas in the hax engine is still ongoing. Some of it is in our new external repo for the core models (https://github.com/cryspen/rust-core-models). And the changes to hax will be published later.

Full list of PRs

Contributors

This Month in Hax: April 2026

In April, we successfully merged 11 pull requests!

Besides various bug fixes, we improved the rendering of ellipsis in pattern-matching and refactored the mvcgen registration of the RustM monad to support partial correctness lemmas. Thanks to the external contributors @MavenRain, @niooss-ledger, and @remix7531!

Towards the end of the month, we participated in the Software Verification in Lean workshop and hackathon organized by the Beneficial AI Foundation and the Lean FRO. We came away with lots of new ideas for our Lean backend.

For a couple of weeks, we have been working on merging the Aeneas engine into Hax. We haven't made any public contributions to Hax in that direction yet, but we have created a new repo containing our Rust core models with an Aeneas-compatible extraction: https://github.com/cryspen/rust-core-models

Full list of PRs

Contributors

This Month in Hax: March 2026

In March, we successfully merged 8 pull requests!

This was a rather quiet month with some of us away for Real World Crypto and HACS in Taipei. But we continued to improve the Lean library with bugfixes and new additions like support for u128 and i128.

Special thanks to @JuanCoRo for improving and extending the treatment of binops in the Lean backend. And thank you @redshiftzero for your first PR!

Full list of PRs

Contributors

This Month in Hax: February 2026

In February, we successfully merged 32 pull requests!

This month, the Lean backend made headway by getting new Rust proof attributes. With the new attributes, Users can choose between two proof methods, one based on symbolic reasoning (grind) and one based on bit-blasting (bv_decide). This new setup is illustrated in our updated Tutorial and the Chacha20 example. Moreover, the Lean backend produces prettier output by opening namespaces, handles for-loops more reliably, and its library contains more of our Rust core models.

We also made great progress on our new THIR importer, implemented in Rust. Currenlty, it can be activated using --experimental-full-def, and we intend to make it the default soon.

Special thanks to @JuanCoRo, @klausnat, and @rusch95 for their contributions this month!

Full list of PRs

Contributors

This Month in Hax: January 2026

In January, we successfully merged 29 pull requests!

The Lean backend continues to get improvements, mostly thanks to @abentkamp! We now have more specs in the proof library (for while loops, negation and more), and improved tactics to reason with our annotations of pre/post-conditions (#1888). Some naming fixes, and improvements of the handling of associated types make us closer to reaching parity with the F* backend!

The other significant improvements are on the core models side. Thanks to some new models, the F* proof library is now entirely extracted from Rust core models. The Rust primitives models will remain hand-written in each backend, but they have been designed to be as small as possible, and are intended to be modelled in a very backend-specific way. The Lean library is now also partly extracted from core models! Some modules are excluded for now as they rely on Rust features that we don't support in the Lean backend for now. We will prioritize these missing features in the next few months to extend the Lean library using extraction from core models.

Full list of PRs

Contributors

Verifying a while loop in Hax/Lean

In our last blog post, the dog that didn't bark was Hax/Lean. It was missing because we did not have support for while loops then. Now, we support them and we will demonstrate it here.

You can find the results of this tutorial on https://github.com/cryspen/rust-gcd/tree/hax_lean1 on the branch hax_lean1.

Preparation

First, we need to install Hax and Lean:

  • Hax (We are using commit d1365d4, so after git clone git@github.com:cryspen/hax.git && cd hax, run git checkout d1365d4)

  • Lean

Again, we will use the gcd Rust crate as an example:

git clone git@github.com:frewsxcv/rust-gcd.git && cd rust-gcd
git checkout 8fb3a59
We add hax-lib as a dependency, which will allow us to make annotations in the Rust code:
cargo add --git https://github.com/hacspec/hax hax-lib --rev d1365d4

Extraction

Now we are ready to translate the Rust code into Lean code. We will limit ourselves to the euclid_u16 function here:

cargo hax into -i '-** +gcd::euclid_u16' lean
This will create a new file proofs/lean/extraction/Gcd.lean containing the Lean version of the extracted function.

For Lean to find the required dependencies, we must add the following two files in proofs/lean:

lean-toolchain:

leanprover/lean4:v4.23.0

lakefile.toml:

name = "Gcd"
version = "0.1.0"
defaultTargets = ["Gcd"]

[[lean_lib]]
name = "Gcd"
roots = ["extraction.Gcd"]

[[require]]
name = "Hax"
path = "../../../hax/hax-lib/proof-libs/lean"
Make sure that the path above points to the subdirectory hax-lib/proof-libs/lean of the repository that you checked out during the installation of Hax (i.e., git@github.com:cryspen/hax.git on commit d1365d4). The path can be relative to the lakefile.toml file or absolute.

Now we can run Lean on the extracted code.

(cd proofs/lean && lake build)
It should take a moment and then say:
Build completed successfully (35 jobs).
So it this already verified? No, currently, we need to add a pre- or post-condition to a function to make Hax generate a specification that we can prove correct. (This will likely change in the near future.)

Verification

We can add the following hax_lib::ensures annoation above the definition of $euclid to say that we want to prove termination and panic-freedom:

#[hax_lib::ensures(|_| true)]
pub const fn $euclid(a: $T, b: $T) -> $T
{
    ...
}
We run Hax and Lean again:
cargo hax into -i '-** +gcd::euclid_u16' lean
(cd proofs/lean && lake build)
Now, we get lots of unsolved goals errors. We can open the Gcd.lean file to get a better impression of what is going on. The file contains a definition of Gcd.euclid_u16, which is the Lean version of our euclid_u16 function and which compiles without error. Below, we have a definition of Gcd.euclid_u16.spec, which contains the specification of the function and an attempted proof of correctness. It should have a red squiggly underline on the contract proof, indicating that the error occurs there. The default proof by mvcgen[Gcd.euclid_u16] <;> try grind fails.

If we click just behind mvcgen[Gcd.euclid_u16], we can see the verification conditions that Lean's mvcgen tactic generated in Lean's infoview. It shows a list of four goals. The second and the forth goal end in:

ToNat.toNat 0 < ToNat.toNat 0
So this says that the u16 value 0, converted to a natural number, is smaller than itself. This is simply wrong and will be impossible to prove. These verification conditions are coming from the default termination measure associated with while loops, which is constant 0 by default. We will have to provide a better measure to prove termination, using the hax_lib::loop_decreases annotation. From our last blog post, we know that b is a useful measure for this loop:
while b != 0 {
    hax_lib::loop_decreases!(b);
    // mem::swap(&mut a, &mut b);
    let temp = a;
    a = b;
    b = temp;

    b %= a;
}
After running Hax and Lean again, the default proof still fails, but the generated verification conditions can now be proved with some manual effort. After developing the proof in Lean, we can copy the proof into the Rust file so that it does not get overwritten when reextracting the code:
#[hax_lib::ensures(|_| true)]
#[hax_lib::lean::proof("by
    mvcgen[Gcd.euclid_u16]
    · expose_names
      intro
      simp_all [a_1]
    · expose_names
      simp only [ToNat.toNat, h_4]
      apply Nat.mod_lt
      grind
    · expose_names
      intro
      simp_all [a_1]
    · expose_names
      simp only [ToNat.toNat, h_4]
      apply Nat.mod_lt
      grind")]
pub const fn $euclid(a: $T, b: $T) -> $T
{
    ...
}
(Be careful with the indentation here! Lean is white-space sensitive!)

After running Hax again, lake build now says:

Build completed successfully (35 jobs).
Yay!

We are working on better automation for proofs like this one and on better coverage of the Rust core library, e.g., to be able to verify the binary gcd implementation in this crate as well.