Move Prover User Guide
This is the user guide for the Move Prover. This document accompanies the Move specification language. See the sections below for details.
Running the Move Proverโ
The Move Prover is invoked via the Aptos CLI. In order to call the CLI, you must have a Move package in place. In the simplest case, a Move package is defined by a directory with a set of .move
files in it and a manifest of the name Move.toml
. You can create a new Move package at a given location by running the command: aptos move init --name <name>
Once the package exists, call the Move Prover from the directory to be tested or by supplying its path to the --package-dir
argument:
- Prove the sources of the package in the current directory:
aptos move prove
* Prove the sources of the package at <path>:
```shell
aptos move prove --package-dir <path>
See example output and other available options in the Proving Move section of Use Aptos CLI.
Target filteringโ
By default, the aptos move prove
command verifies all files of a package. During iterative development of larger packages, it is often more effective to focus verification on particular files with the
-f
(--filter
) option, like so:
aptos move prove -f coin
In general, if the string provided to the -f
option is contained somewhere in the file name of a source, that source will be included for verification.
NOTE: the Move Prover ensures there is no semantic difference between verifying modules one-by-one or all at once. However, if your goal is to verify all modules, verifying them in a single
aptos move prove
run will be significantly faster than sequentially.
Prover optionsโ
The Move Prover has a number of options (such as the filter option above) that you pass with an invocation of: aptos move prove <options>
. The most commonly used option is the -t
(--trace
) option that causes the Move Prover to produce richer diagnosis when it encounters errors:
aptos move prove -f coin -t
To see the list of all command line options, run: aptos move prove --help
Prover configuration fileโ
You can also create a Move Prover configuration file named Prover.toml
that lives side-by-side with the Move.toml
manifest file in the root of the package directory. For example, to enable tracing by default for a package, add a Prover.toml
file with the following configuration:
[prover]
auto_trace_level = "VerifiedFunction"
Find the most commonly used options in the example .toml
below, which you can cut and paste and adopt for your needs (adjusting the defaults shown in the displayed values as needed):
# Verbosity level
# Possible values: "ERROR", "WARN", "INFO", "DEBUG". Each level subsumes the output of the previous one.
verbosity_level = "INFO"
[prover]
# Set auto-tracing level, which enhances the diagnosis the Move Prover produces on verification errors.
# Possible values: "Off", "VerifiedFunction", "AllFunctions"
auto_trace_level = "Off"
# Minimal severity level for diagnosis to be reported.
# Possible values: "Error", "Warning", "Note"
report_severity = "Warning"
[backend]
# Timeout in seconds for the solver backend. Note that this is a soft timeout and may not always
# be respected.
vc_timeout = 40
# Random seed for the solver backend. Different seeds can result in different verification run times,
# as the solver uses heuristics.
random_seed = 1
# The number of processor cores to assume for concurrent check of verification conditions.
proc_cores = 4
HINT: For local verification, you may want to set
proc_cores
to an aggressive number (your actual cores) to speed up the turnaround cycle.
Prover diagnosisโ
When the Move Prover finds a verification error, it prints diagnosis to standard output in a style similar to a compiler or a debugger. We explain the different types of diagnoses below, based on the following evolving example:
module 0x0::m {
struct Counter has key {
value: u8,
}
public fun increment(a: address) acquires Counter {
let r = borrow_global_mut<Counter>(a);
r.value = r.value + 1;
}
spec increment {
aborts_if !exists<Counter>(a);
ensures global<Counter>(a).value == old(global<Counter>(a)).value + 1;
}
}
We will modify this example as we demonstrate different types of diagnoses.
Unexpected abortโ
If we run the Move Prover on the example immediately above, we get the following error:
error: abort not covered by any of the `aborts_if` clauses
โโ m.move:11:5
โ
8 โ r.value = r.value + 1;
โ - abort happened here with execution failure
ยท
11 โ โญ spec increment {
12 โ โ aborts_if !exists<Counter>(a);
13 โ โ ensures global<Counter>(a).value == old(global<Counter>(a)).value + 1;
14 โ โ }
โ โฐโโโโโ^
โ
= at m.move:6: increment
= a = 0x29
= at m.move:7: increment
= r = &mmm.Counter{value = 255u8}
= at m.move:8: increment
= ABORTED
{
"Error": "Move Prover failed: exiting with verification errors"
}
The Move Prover has generated an example counter that leads to an overflow when adding 1 to the value of 255 for an u8
. This overflow occurs if the function specification calls for abort behavior, but the condition under which the function is aborting is not covered by the specification. And in fact, with aborts_if !exists<Counter>(a)
, we only cover the abort caused by the absence of the resource, but not the abort caused by the arithmetic overflow.
Let's fix the above and add the following condition:
spec increment {
aborts_if global<Counter>(a).value == 255;
...
}
With this, the Move Prover will succeed without any errors.
Post-condition failureโ
Let us inject an error into the ensures
condition of the above example:
spec increment {
ensures global<Counter>(a).value == /*old*/(global<Counter>(a).value) + 1;
}
With this, the Move Prover will produce the following diagnosis:
error: post-condition does not hold
โโ m.move:14:9
โ
14 โ ensures global<Counter>(a).value == /*old*/(global<Counter>(a).value) + 1;
โ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
โ
= at m.move:6: increment
= a = 0x29
= at m.move:7: increment
= r = &mmm.Counter{value = 0u8}
= at m.move:8: increment
= at m.move:9: increment
= at m.move:12: increment (spec)
= at m.move:15: increment (spec)
= at m.move:13: increment (spec)
= at m.move:14: increment (spec)
{
"Error": "Move Prover failed: exiting with verification errors"
}
While we know what the error is (as we just injected it), this is not particularly obvious in the output This is because we don't directly see on which values the ensures
condition was actually evaluated. To see
this, use the -t
(--trace
) option; this is not enabled by default because it makes the verification problem slightly harder for the solver.
Instead or in addition to the --trace
option, you can use the built-in function TRACE(exp)
in conditions to explicitly mark expressions whose values should be printed on verification failures.
NOTE: Expressions that depend on quantified symbols cannot be traced. Also, expressions appearing in specification functions can not currently be traced.
Debugging the Move Proverโ
The Move Prover is an evolving tool with bugs and deficiencies. Sometimes it might be necessary to debug a problem based on the output the Move Prover passes to the underlying backends. If you pass the option --dump
, the Move Prover will output the original Move bytecode, as well as the Move Prover bytecode, as the former is transformed during compilation.