Blaster provides an SMT backend for Z3 proofs. Blaster works by first aggressively optimizing the Lean expression of a theorem, sometimes up to a True goal, before sending the remaining goal and context to an SMT solver.
- Table of Contents
- Installation
- How to use?
- Features
- Examples
- Benchmarks
- General description of Blaster
- Installing the Z3 Solver
- Contributing
Blaster is built with the philosophy that fewer dependencies mean better maintainability and more optimization opportunities. That said, Blaster requires:
- Lean4 v4.24.0 (or compatible version)
- Z3 v4.15.2 (or compatible version)
We strive to stay in sync with the latest stable release of Lean4.
Currently supported version: Lean4 v4.24.0
Please follow the official installation guidelines from the Lean4 GitHub repository.
We do our best to stay updated with the latest release of Z3. However, regressions can occur and often require extensive research and resolution, so Blaster might be slightly behind the latest version.
Currently tested version: Z3 v4.15.2
Note: Blaster should work with later releases, though no guarantees are made.
The section on Installing the Z3 Solver below explains how to get the right version of Z3 installed and check that Lean is using that version. If you need more help, please see the official installation guidelines from the Z3 GitHub repository.
In order to use Blaster, your project needs to depend on lean-blaster.
If you use lakefile.toml then, simply add a dependency to this repository:
[[require]]
name = "Blaster"
git = "https://github.com/input-output-hk/Lean-blaster"
rev = "main"If you use lakefile.lean then, simply add a dependency to this repository:
require «Blaster» from git
"https://github.com/input-output-hk/Lean-blaster" @ "main"
timeout: specifying the timeout (in second) to be used for the backend smt solver (defaut: ∞)verbose:activating debug info (default: 0)only-smt-lib: only translating unsolved goals to smt-lib without invoking the backend solver (default: 0)only-optimize: only perform optimization on lean specification and do not translate to smt-lib (default: 0)dump-smt-lib: display the smt lib query to stdout (default: 0)random-seed: seed for the random number generator (default: none)gen-cex: generate counterexample for falsified theorems (default: 1)solve-result: specify the expected result from the #blaster command, i.e., 0 for 'Valid', 1 for 'Falsified' and 2 for 'Undetermined'. (default: 0)
You can call the solver by invoking the #blaster command on a theorem name or on a propositional expression.
The syntax is as follows:
#blaster (option1: n) (option2: n) [theoremName]; or#blaster (option1: n) (option2: n) [theoremBody]
For example,
theorem addCommute : ∀ (a b : Nat), a + b = b + a := by sorry
#blaster (only-optimize: 1) (solve-result: 0) [addCommute]
-- or
#blaster (only-optimize: 1) (solve-result: 0) [∀ (a b : Nat), a + b = b + a]
-- or
#blaster [∀ (a b : Nat), a + b = b + a]The solver can also be invoked via the blaster tactic. This tactic can be combined with other Lean4 tactics when trying to prove a theorem.
The syntax is as follows: by blaster (option1: n) (option2: n).
For example,
theorem addCommute : ∀ (a b : Nat), a + b = b + a := by
blaster (only-optimize: 1)
-- or
theorem length_set {as : List α} {i : Nat} {a : α} : (as.set i a).length = as.length := by
induction as generalizing i <;> blasterNote
The tool does not perform proof reconstruction right now.
- When the solver declares a goal as
Valid, the tactic currently concludes the proof with anadmit. - When the solver declares a goal as
Falsified, the tactic fails and a counterexample is provided as witness. No counterexample is provided when a goal is reduced toFalseat the optimization phase. - When the solver returns
Undetermined(i.e., the back-end solver was not able to prove/refute the goal), the tactic returns the current goal to be solved.
inductive Either (α : Type u) (β : Type v) where
| Left : α -> Either α β
| Right : β -> Either α β
def isLeft : Either a b -> Bool
| Either.Left _ => true
| Either.Right _ => false
def isRight : Either a b -> Bool
| Either.Left _ => false
| Either.Right _ => true
theorem isLeft_not_isRight_iff : ∀ (x : Either α β), ¬ (isRight x) = isLeft x := by blastermutual
inductive A
| self : A → A
| other : B → A
| empty
inductive B
| self : B → B
| other : A → B
| empty
end
mutual
def A.sizeA : A → Nat
| .self a => a.sizeA + 1
| .other b => b.sizeB + 1
| .empty => 0
def B.sizeB : B → Nat
| .self b => b.sizeB + 1
| .other a => a.sizeA + 1
| .empty => 0
end
theorem A_self_size (a : A) : (A.self a).sizeA = a.sizeA + 1 := by blaster#blaster [ ∀ (x : Nat) (xs : List Nat), List.length xs + 1 = List.length (x :: xs) ]mutual
def isEven : Nat → Bool
| 0 => true
| n+1 => isOdd n
def isOdd : Nat → Bool
| 0 => false
| n+1 => isEven n
end
#blaster [ ∀ (n : Nat), isEven (n+1) = isOdd n ]
#blaster [ ∀ (n : Nat), isEven (n+2) → isEven n ]inductive Either (α : Type u) (β : Type v) where
| Left : α -> Either α β
| Right : β -> Either α β
instance [BEq a] [BEq b] : BEq (Either a b) where
beq | Either.Left a1, Either.Left a2 => a1 == a2
| Either.Right b1, Either.Right b2 => b1 == b2
| _, _ => false
#blaster
[ (∀ (α : Type) (a b : α), [BEq α] → a == b → a = b) →
(∀ (α : Type) (β : Type) (x y : Either α β), [BEq α] → [BEq β] → x == y → x = y)
]#blaster [ (∀ (β : Type) (x : Term (List β)) (f : Term (List β) → Nat), f x > 10) →
(∀ (α : Type) (x y : Term (List α)) (f : Term (List α) → Nat), f x + f y > 20)
]#blaster [ ∀ (x : Nat) (xs : List Nat), !(List.isEmpty xs) →
List.head! (List.map (Nat.add x) xs) ≥ x
]def sizeOfTerm (t : Term α) : Nat :=
match t with
| .Ident _ => 1
| .Seq xs => List.length xs
| .App _ args => List.length args
| .Annotated t' _ => 1 + sizeOfTerm t'
#blaster [ ∀ (α : Type) (x : Term α), sizeOfTerm x < 10 ]
❌ Falsified
Counterexample:
- x: (let ((a!1 (Test.SmtPredQualifier.Term.Annotated
(Test.SmtPredQualifier.Term.Annotated
(Test.SmtPredQualifier.Term.Annotated
(Test.SmtPredQualifier.Term.Ident "!a!")
(as List.nil (@List (@Test.SmtPredQualifier.Attribute @@Type))))
(as List.nil (@List (@Test.SmtPredQualifier.Attribute @@Type))))
(as List.nil (@List (@Test.SmtPredQualifier.Attribute @@Type))))))
(let ((a!2 (Test.SmtPredQualifier.Term.Annotated
(Test.SmtPredQualifier.Term.Annotated
(Test.SmtPredQualifier.Term.Annotated
(Test.SmtPredQualifier.Term.Annotated
a!1
(as List.nil
(@List (@Test.SmtPredQualifier.Attribute @@Type))))
(as List.nil (@List (@Test.SmtPredQualifier.Attribute @@Type))))
(as List.nil (@List (@Test.SmtPredQualifier.Attribute @@Type))))
(as List.nil (@List (@Test.SmtPredQualifier.Attribute @@Type))))))
(Test.SmtPredQualifier.Term.Annotated
(Test.SmtPredQualifier.Term.Annotated
a!2
(as List.nil (@List (@Test.SmtPredQualifier.Attribute @@Type))))
(as List.nil (@List (@Test.SmtPredQualifier.Attribute @@Type))))))instance counterStateMachine : StateMachine Request CounterState where
init _ := { state := .Ready, timer := 0}
next i s :=
match s.state with
| .Ready =>
match i with
| .Tr => { state := .Delay, timer := 0}
| _ => s
| .Delay =>
if s.timer < 3
then {s with timer := s.timer + 1}
else {s with state := .Busy }
| .Busy =>
match i with
| .Fa => {s with state := .Ready}
| _ => s
assumptions _ _ := True -- no assumptions
invariants _ s :=
(s.timer > 0 → s.timer < 3 → s.state = .Delay) ∧
s.timer ≥ 0 ∧
s.timer ≤ 3Command #bmc is provided to search for counterexamples up to a specified depth k on a given state machine instance.
When no provided, depth k defaults to 10.
For example, a counterexample is detected at Depth 4, when invariant s.timer ≤ 3 is changed to s.timer < 3
#bmc (max-depth: 8) (verbose: 1) [counterStateMachine]
❌ Falsified
Counterexample detected at Depth 4:
- «Test.Counter02.counterStateMachine.input@1»: Test.Counter02.Request.Tr
- «Test.Counter02.counterStateMachine.input@2»: Test.Counter02.Request.Fa
- «Test.Counter02.counterStateMachine.input@3»: Test.Counter02.Request.Fa
- «Test.Counter02.counterStateMachine.input@4»: Test.Counter02.Request.Tr
BMC at Depth 0
BMC at Depth 1
BMC at Depth 2
BMC at Depth 3
BMC at Depth 4Command #kind is provided to prove that a state machine's invariants are always satisfied.
It basically conducts an inductive proof in which the base case is handled via BMC, and the step case verifies that
whenever the invariants hold for an arbitrary state, they must also hold for all states reachable from it.
#kind (max-depth: 1) (verbose: 2) [counterStateMachine]
✅ Valid
KInd at Depth 0
KInd at Depth 1Indexed inductive data types are not yet supported because they lack a native representation at SMT-LIB level. We expect to add support soon via a suitable encoding that faithfully preserves the Lean4 semantics. For example,
inductive Finn : Nat → Type where
| fzero : {n : Nat} → Finn n
| fsucc : {n : Nat} → Finn n → Finn (n+1)Inductive predicates are not yet supported, but our plan is to enable them by translating each predicate into an equivalent boolean function at SMT-LIB level.
Blaster does not currently attempt induction on its own.
Users can work around this by pairing blaster with the induction tactic in Lean4.
We plan to enhance this by introducing heuristics that enable automatic inductive reasoning.
For example,
inductive Path where
| Here : Path
| There : Path -> Path
def check_valid_path {α : Type}[BEq α](v : α)(p : Path)(ls : List α)
: Bool
:= match p , ls with
| .Here , .cons l _ls => v == l
| .There rs , .cons _ ls => check_valid_path v rs ls
| _ , _ => false
theorem validProof {α : Type}[BEq α](v : α)(p : Path)(ls : List α)
: check_valid_path v p ls == true -> List.elem v ls := by
induction ls generalizing p <;> blasterCurrently, Blaster does not perform case analysis to split a goal into subgoals.
Users can address this by using the blaster tactic alongside Lean4’s by_cases tactic.
Our plan is to support automatic goal decomposition so that smaller SMT queries are
generated instead of one monolithic query. This will highlight the harder subgoals
and make them simpler for users to examine manually.
Examples are provided in the Tests folder.
The Tests/FixedIssues folder contains examples that were, at some point, not properly handled by our tool.
The Tests/Optimize folder contains examples of just the optimization step of the tool.
The Tests/Smt/Benchmarks/ValidatorsExamples contains simplified examples of Cardano validators. It contains two examples HelloWorld and Vesting.
The Tests/StateMachine folder contains example on how to use the state machine formalization.
Blaster has been benchmarked against a variety of well-known benchmarks to evaluate its performance and correctness. The evaluation can be found on this public repository: Blaster-benchmarking
Lean Natural Number Game
- Repository: NNG4
- Results: 103/117
- Notes:
- Failed on the examples that are not considered theorems by Lean
- Failed on most of the Power examples
- It includes Fermat's Last Theorem so 100% is unlikely to happen
Lean Set Theory Game
- Repository: STG4
- Results: 51/52
- Notes:
- Failed on the
singletontheorem from FamCombo
- Failed on the
Verina.io
- Repository: [Add link here]
- Results: [Add results here]
"Lean-Book"
- Repository: [Add link here]
- Results: [Add results here]
Blaster uses a three-step process to automatically reason about Lean theorems.
Before translation to SMT-LIB, blaster optimizes the Lean expression (see Blaster/Optimize/Basic.lean). This step simplifies the expression and prepares it for SMT translation by applying various transformations and rewriting rules, which can significantly improve the SMT solver's performance.
These rules are applied recursively to the expression tree and are designed to reduce the complexity of the SMT query.
The core optimization logic is orchestrated in Blaster/Optimize/Basic.lean, which applies a variety of strategies, including:
- Beta Reduction: Lambda applications are simplified by substituting arguments into the lambda body.
- Function Unfolding: Non-recursive and non-opaque functions are unfolded to their definitions.
- Let-Expression Inlining: Let-bindings are inlined to simplify the expression.
In addition to these general strategies, Lean-blaster applies a set of specific rewriting rules for different types of expressions, primarily located in the Blaster/Optimize/Rewriting/ directory. We give a few examples in this section to illustrate the goal of those rules.
Boolean expressions are simplified using a set of rules. These rules include:
- Identity:
true && esimplifies toe, andfalse || esimplifies toe. - Annihilation:
false && esimplifies tofalse, andtrue || esimplifies totrue. - Constants:
e && not esimplifies tofalse, ande || not esimplifies totrue. - Hypothesis-based simplification: If an expression is known to be true or false from the current context, it is simplified accordingly.
Arithmetic operations on natural numbers are optimized using a variety of algebraic simplifications and constant folding rules, which can be found in Blaster/Optimize/Rewriting/OptimizeNat.lean. These include:
- Constant Folding: Expressions with constant values are evaluated (e.g.,
2 + 3is replaced with5). - Identity and Annihilation:
0 + nsimplifies ton.n - 0simplifies ton.1 * nsimplifies ton.0 * nsimplifies to0.- And many more.
- Algebraic Simplifications: More complex rules are applied, such as:
(m * n) / m, wherenandmare expressions, simplifies tonifmis known to be non-zero in the current context.
- Normalization: Arguments to commutative operators like
Nat.addandNat.mulare reordered to a canonical form, which helps in identifying further optimization opportunities.
These are just a few examples of the many optimization rules that Blaster applies to simplify expressions before they are sent to the SMT solver. This pre-processing step is crucial for the tool's performance and allows it to handle more complex verification tasks.
Blaster also includes a set of rules for simplifying control flow expressions like if-then-else (ITE), dependent if-then-else (DITE) and match expressions. These rules, found in Blaster/Optimize/Rewriting/OptimizeITE.lean and Blaster/Optimize/Rewriting/OptimizeMatch.lean, are designed to reduce the complexity of the expression by eliminating redundant branches and propagating constants.
-
ITE/DITE Simplification:
if-then-elseexpressions are simplified in several ways:- If the condition is a constant (
trueorfalse), the expression is replaced with the corresponding branch. - If the
thenandelsebranches are equivalent expressions, the wholeif-then-elseis replaced with that branch. - And many more.
- If the condition is a constant (
-
Match Expression Optimization:
matchexpressions are optimized by:- Constant Propagation: If a discriminator (the value being matched on) is a known constant, the
matchexpression is replaced with the corresponding branch. - Unreachable Branch Elimination: If a branch is determined to be unreachable based on the current context, it is eliminated.
- Normalization: In some cases,
matchexpressions are normalized into a series ofif-then-elseexpressions to enable further simplification.
- Constant Propagation: If a discriminator (the value being matched on) is a known constant, the
Function propagation is another key optimization strategy, detailed in Blaster/Optimize/Rewriting/FunPropagation.lean. This technique simplifies expressions by "pushing" function calls into their arguments. For example, a function applied to an if-then-else expression can be transformed into an if-then-else expression where the function is applied to both the then and else branches. This can be particularly effective when one of the branches can be further simplified after the function is applied using other optimization rules.
Using the whole set of optimization rules, it may happen that a theorem can be reduced to True. This concludes the proof and the theorem is considered as Valid. For some other cases, a theorem may also be reduced to False and will therefore be declared as Falsified. Most of the time, a proof might not be concluded at the optimization phase. In this case, the optimized Lean expression is translated into an SMT-LIB format and submitted to the backend solver.
The translation step is handled in Blaster/Smt/Translate.lean. This process involves several key steps:
- Expression Traversal: The tool recursively traverses the Lean expression tree.
- Type and Function Translation: Lean types and functions are mapped to their SMT-LIB equivalents.
- Quantifier Handling: Universal and existential quantifiers are translated into SMT-LIB quantifiers.
- Application Translation: Function applications are translated into SMT-LIB function calls.
Once an expression has been translated, Blaster interacts with an external SMT solver (i.e., Z3) to verify the SMT-LIB formula. This is done by asserting the negation of the formula to determine its satisfiability. The results are interpreted as follows:
- unsat: The original expression is valid.
- sat: The original expression is falsified, and a counterexample may be generated.
- unknown: The solver could not determine the validity of the expression.
Blaster requires Z3 version 4.15.2. To install that, you need to
- Check out the 4.15.2 tagged branch of the Z3 repo;
- Install Z3 in a location that doesn't conflict with possible existing versions
on your machine (e.g., in
/usr/bin/z3) - Ensure Lean 4 is using the right version of Z3.
Below are instructions for accomplishing these objectives. (They are aimed at .deb-based Linux, but the same or similar steps should work on other platforms.)
Z3 releases are tagged on GitHub; the tag you want is z3-4.15.2.
1.1 Install build dependencies
sudo apt update
sudo apt install -y build-essential python3 git1.2 Clone the 4.15.2 tag
Do not clone the master branch; it will give you a newer version (e.g., 4.15.4) that we do not yet fully support. Instead:
# Shallow clone just the 4.15.2 tag, into a directory named z3-4.15.2
git clone --branch z3-4.15.2 --depth 1 https://github.com/Z3Prover/z3.git z3-4.15.2
cd z3-4.15.2
# Sanity check: this should print something like "z3-4.15.2"
git describe --tags1.3 Configure build with a safe prefix
By default, mk_make.py uses a prefix like /usr, which can clash with files installed by
package managers (e.g., apt). The Z3 README documentation recommends using
--prefix to choose a custom install directory, typically /usr/local, as follows:
python3 scripts/mk_make.py --prefix=/usr/local
cd build
make -j"$(nproc)"
sudo make installThis installs
z3to/usr/local/bin/z3- libraries to
/usr/local/lib - header files to
/usr/local/include
Check which z3 you’re actually picking up:
which z3
z3 --versionIdeally you will see
/usr/local/bin/z3 and Z3 version 4.15.2 - 64 bit.
If which z3 shows something else, e.g., /usr/bin/z3, then /usr/local/bin isn’t ahead
of /usr/bin in your PATH; fix this by either removing the version of z3 that's
in /usr/bin (use sudo apt remove z3 if you installed the old version of Z3 with
apt) or by adding the following to your shell config file (~/.bashrc, ~/.zshrc, etc.):
export PATH=/usr/local/bin:$PATHThen reload your shell and re-run which z3.
Lean just calls z3 as an external process (via IO.Process or tactics that use Z3).
It doesn’t have its own embedded Z3. So:
If the shell you run lake from sees /usr/local/bin/z3 (4.15.2), then Lean will also use 4.15.2.
Just to be sure, you can run the simple test we provide in this repository, as follows:
lake build z3check
lake exe z3checkIf Z3 is installed correctly, you should see the following output:
Successfully ran z3:
Z3 version 4.15.2 - 64 bit
We welcome all contributions! Whether it's bug reports, feature requests, documentation improvements, or code contributions, your help is appreciated.
- Report bugs and issues
- Suggest new features or improvements
- Improve documentation
- Submit pull requests
Maintained by:
Questions? Feel free to open an issue or reach out to the maintainers.