Operational guide. This document explains how Verus deductive verification is applied to
VerifiedStateMachinetypes in this codebase: what problems were encountered, what the current design is, and how to run and extend it.For the architecture of VSMs themselves (layers, traits, type theory), see
VERIFIED_STATE_MACHINES.md. For the Kani model-checking backend, seeKANI_FOR_VSMS.md. For the Creusot deductive backend, seeCREUSOT_FOR_VSMS.md. For the Verus proof infrastructure (running, syntax, tiers), seeVERUS_GUIDE.md.
| Backend | Method | Scope | VSM role |
|---|---|---|---|
| Kani | Bounded model checking (CBMC) | All variants, depth ≤ 2 | Memory-safety, no-panic, bit-level |
| Creusot | Deductive via WhyML + Alt-Ergo | All inputs, unbounded | Functional correctness, loop invariants |
| Verus | Unbounded SMT via Z3 | All inputs, unbounded | Rich contracts, ghost state, type invariants |
Verus and Creusot both offer unbounded proofs through different toolchains. A property verified by all three has been checked by three completely independent verification paths.
| Issue | Kani workaround | Creusot workaround | Verus: no workaround needed |
|---|---|---|---|
| Heap types in enums | One harness per variant × 3 depths | Companion functions | Z3 ADT theory — single assume_specification |
String fields |
kani::any_vec depth harnesses |
extern_spec! per crate |
vstd full specs: s@ == Seq::<char>::empty() |
| Cross-module predicate opacity | N/A | Inline const INV_* in generated file |
pub open spec fn — opt-in transparency |
| Cross-crate fn specs | N/A | extern_spec! must be in calling crate |
assume_specification works from any crate |
#[verifier::type_invariant]— invariant automatically checked at every construction and field mutation; no Kani/Creusot equivalent (gallery V8).Tracked<T>— linear ghost tokens that thread proof witnesses through transition chains (gallery V6, V10).spec fn/proof fn/exec fnthree-mode hierarchy with explicit body-visibility tiers (pub open/pub closed/#[verifier::opaque]).
Verus cannot verify production VSM transitions directly — they are async fns
that use tokio, database drivers, and other runtime machinery. The generated
companion architecture uses three pieces working together:
A #[verifier::external] stub is declared as a plain Rust function with a
todo!() body. It has the simplified signature of the transition (state in →
state out), stripping async and extra runtime arguments:
// Outside the verus! block — regular Rust, #[verifier::external]
#[verifier::external]
pub fn apply_filter_stub(state: ArchiveNavState) -> ArchiveNavState {
todo!()
}The #[verifier::external] attribute tells Verus to treat this function as
external — it will be given its contract via assume_specification.
Inside the verus! { } block, assume_specification attaches a trusted
postcondition to the stub. Verus treats this as an axiom — it does not verify
the body, only the contract:
verus! {
pub assume_specification [apply_filter_stub](
state: ArchiveNavState,
) -> (r: ArchiveNavState)
requires archive_nav_consistent(&state),
ensures archive_nav_consistent(&r),
;
}This is the trust anchor: "the real apply_filter satisfies this contract
because Kani and Creusot verify it independently."
A verified exec function calls the stub and proves the invariant is preserved.
Verus uses the assume_specification ensures to discharge the proof goal:
verus! {
pub fn apply_filter_verified(
state: ArchiveNavState,
) -> (r: ArchiveNavState)
requires archive_nav_consistent(&state),
ensures archive_nav_consistent(&r),
{
let r = apply_filter_stub(state);
r
}
}Z3 chains: assume_specification ensures archive_nav_consistent(&r) →
verified fn's ensures is satisfied. No manual proof hints needed.
- Kani and Creusot independently verify the real transition contracts on the
production code. Those contracts are the same postconditions asserted in
assume_specification. - Verus trusts
assume_specificationand proves invariant preservation. - All three backends satisfy the same proof obligation via different paths.
| V9/V10 (old) | V13 (current) | |
|---|---|---|
| Spec attachment | assume_specification on production fn directly |
assume_specification on #[verifier::external] stub |
| Verified exec caller | Not generated | Generated — explicitly proves invariant preservation |
| Connection to Kani/Creusot | Indirect (abstract spec) | Direct — same postcondition predicates |
Companions are auto-generated by crates/elicitation/src/cli/generate/verus_gen.rs
via the elicitation generate command. You never write them by hand.
To regenerate after changing a VSM:
cargo build --release -p elicitation --features cli
cp target/release/elicitation ~/.cargo/bin/elicitation
elicitation generate # regenerates all backends including VerusGenerated files live in the elicitation_verus crate, which is built with
vargo (excluded from the normal cargo workspace):
crates/elicitation_verus/src/generated/
├── mod.rs # Declares the four per-machine modules
├── archive_connection.rs # ArchiveConnectionMachine companions
├── archive_nav.rs # ArchiveNavMachine companions
├── archive_overlay.rs # ArchiveOverlayMachine companions
└── archive_panel.rs # ArchivePanelMachine companions
Each file has a three-part structure:
// 1. External stubs (outside verus! { })
// — #[verifier::external] fns with todo!() bodies
// 2. verus! { ... } block:
// a. Abstract state enum + pub open spec fn consistent(...)
// b. Postcondition predicates (trivial / passthrough / conditional)
// c. Leaf lemmas
// d. assume_specification blocks (one per transition)
// e. Verified exec callers (one per transition)
// f. Tag enum + composition proof (over all transitions simultaneously)
verus_gen.rs drives generation from the machine's #[formal_method]
metadata. For each transition the generator emits:
- A stub via
TransKind::external_stub()→ placed beforeverus! { - An
assume_specificationviaTransKind::assume_spec_ensures()→ insideverus! { - A verified exec caller via
TransKind::verified_caller()→ insideverus! {
The composition proof (tag enum dispatch) is emitted once per machine and covers all transitions simultaneously via Verus's Z3 ADT theory.
Each machine's invariant type carries a verus_invariant_fn attribute
so the generator knows which spec function to use:
#[prop(
kani_invariant_fn = "archive_connection_consistent",
creusot_invariant_fn = "archive_connection_consistent",
verus_invariant_fn = "archive_connection_consistent",
)]
pub struct ArchiveConnectionConsistent;In Verus, a pub open spec fn defined in crate A is syntactically visible
from crate B, but its body is only transparent if:
- The producer exports it with
--export CRATE=pathflag, and - The consumer imports it with
--import CRATE=pathflag.
Without those flags, an open spec fn imported from another crate behaves as
uninterpreted: the prover sees the signature but not the body, so it cannot
unfold it to prove postconditions.
build.rs defines each invariant predicate as a raw string constant
(VERUS_INV_*) and embeds it verbatim in the generated companion file.
This means the pub open spec fn archive_connection_consistent is declared
in archive_connection.rs itself, not imported from elsewhere.
Verus sees the body and can unfold it during verification without any
--export/--import flag dance.
This is analogous to the Creusot fix (inlining #[logic] predicates into
each companion file), but simpler: no const &str roundtrip through
prettyplease is needed because Verus files are written as raw strings
(Verus syntax is not valid Rust, so syn::parse_file would fail).
Production VSM transitions are async fn. Verus is a synchronous verifier —
it cannot model async/await semantics.
assume_specification does not require Verus to call or verify the function
body. It only annotates the synchronous signature as a trusted axiom.
The async-ness is irrelevant; the contract is about the relationship between
the input state and the output state regardless of how the transition is executed.
// Production transition (async, not verified):
pub async fn begin_connect_sql(
state: ArchiveConnectionState,
proof: Established<ArchiveConnectionConsistent>,
profile_name: String, backend: BackendKind,
) -> (ArchiveConnectionState, Established<ArchiveConnectionConsistent>) { ... }
// Companion contract (sync signature, trusted axiom):
#[cfg(verus)]
verus! {
pub assume_specification [begin_connect_sql] (
_state: ArchiveConnectionState,
proof: Established<ArchiveConnectionConsistent>,
profile_name: String, backend: BackendKind,
) -> (r: (ArchiveConnectionState, Established<ArchiveConnectionConsistent>))
requires archive_connection_consistent(&_state),
ensures archive_connection_consistent(&r.0),
;
}The gallery in crates/elicitation_verus/src/gallery/ is a thirteen-level
learning curriculum that validates each Verus capability needed by the
production VSM companions before applying it to real archive code.
| Level | File | Theme | Key concept |
|---|---|---|---|
| V1 | level1.rs |
Unit type baseline | pub open spec fn, basic requires/ensures |
| V2 | level2.rs |
Two-variant enum | Z3 ADT theory, pattern-match in spec fn |
| V3 | level3.rs |
Enum with u64 field |
Arithmetic invariant, overflow guard |
| V4 | level4.rs |
Spec fn visibility tiers | pub open vs pub closed vs #[verifier::opaque] |
| V5 | level5.rs |
String fields |
vstd string model (s@ == Seq::<char>::empty()) |
| V6 | level6.rs |
Tracked<WfToken> |
Linear ghost tokens, proof fn mint/advance/consume |
| V7 | level7.rs |
Full VSM pattern | Multi-variant enum + String + u64, multiple transitions |
| V8 | level8.rs |
#[verifier::type_invariant] |
Auto-checked invariant, private fields, spec accessors |
| V9 | level9.rs |
assume_specification |
Trusted axiom for external/async fn |
| V10 | level10.rs |
Proof composition | Tracked<T> through two transitions, exec compose |
| V11 | level11.rs |
Leaf + composition | Full proof: leaf lemmas + composition proof over all transitions |
| V12 | level12.rs |
Multi-machine composition | Composition across two cooperating state machines |
| V13 | level13.rs |
Production reference | #[verifier::external] stub + assume_specification + verified exec caller |
V7 is the invariant level: proves a production-shaped state machine fully.
V13 is the V13 reference: the pattern used by all generated companions.
Read level13.rs before modifying verus_gen.rs.
All levels pass: verification results:: 780 verified, 0 errors (or greater).
The recommended invocation handles all environment setup automatically:
elicitation prove --verus --csvThis runs vargo on crates/elicitation_verus/src/lib.rs and writes results
to ./verus_verification_results.csv. The command exits non-zero on any failure.
just verify-verusThis runs Verus on crates/elicitation_verus/src/lib.rs (the gallery crate,
which is excluded from the normal workspace). Expected output:
verification results:: 780 verified, 0 errors (or greater).
just verify-verus-tracked # Full run, CSV output
just verify-verus-resume results.csv # Resume (skip passed)
just verify-verus-summary results.csv # Show statisticsjust check elicit_proofsThis compiles elicit_proofs including the generated companion files. The
companions contain only #[cfg(verus)]-gated code, so the normal Rust
compiler silently ignores the verus! { } blocks. The check confirms that
all imports, types, and mod declarations are correct.
-
Add
verus_invariant_fnto the#[prop]attribute on the machine's invariant type:#[prop(verus_invariant_fn = "my_machine_consistent")] pub struct MyMachineConsistent;
-
Add a
VERUS_INV_*constant inbuild.rswith the invariant predicate body:const VERUS_INV_MY_MACHINE: &str = "\ #[cfg(verus)] verus! { pub open spec fn my_machine_consistent(_state: &MyMachineState) -> bool { true } }";
-
Call
write_verus_vsm_filefromgenerate_verus_companions()for the new machine, passingMyMachine::default().transition_verus_contracts("my_machine_consistent"). -
Add the new module to
src/verus/generated/mod.rs:pub mod my_machine;
-
Add a placeholder file
src/verus/generated/my_machine.rs:// AUTO-GENERATED — see build.rs -
Rebuild:
cargo build -p elicit_proofs
Currently all VERUS_INV_* constants return true. When you add a real
invariant, model it as a pub open spec fn with a match on the state enum:
const VERUS_INV_MY_MACHINE: &str = "\
#[cfg(verus)]
verus! {
pub open spec fn my_machine_consistent(state: &MyMachineState) -> bool {
match state {
MyMachineState::Idle => true,
MyMachineState::Active { name } => name@.len() > 0,
}
}
}";Key rules:
- Use
s@forStringfields (s@ == Seq::<char>::empty()for empty,s@.len() > 0for non-empty). - Use
*count > 0foru64fields (the*dereferences the pattern binding). - Import
vstd::prelude::SpecOrdif you use>or<in the body.
| Phase | Status | Description |
|---|---|---|
| Phase 1 | ✅ Done | Gallery V1–V6 (toolchain, patterns, ghost tokens) |
| Phase 2 | ✅ Done | Gallery V7–V10 (full VSM, type_invariant, compose) |
| Phase 3 | ✅ Done | vsm_verus_proof() infra + auto-generated companion files |
| Phase 4 | ✅ Done | Real invariant bodies in generated companions |
| Phase 5 | ✅ Done | Standalone Verus VSM contracts via strictly_verus crate |
| Phase 6 | ✅ Done | Archive machines verified in elicitation_verus via V13 pattern |
| Gallery V11–V13 | ✅ Done | Leaf+composition (V11), multi-machine (V12), V13 reference impl |
Phase 6 upgraded verus_gen.rs to the V13 pattern. Archive companions now live
in crates/elicitation_verus/src/generated/ and are verified by vargo:
elicitation prove --verus --csv # 4/4 machines, all passingEach generated file contains:
#[verifier::external]stubs (one per transition) — beforeverus! { }assume_specificationblocks — axiomatize the real contracts- Verified exec callers — Verus proves invariant preservation from axioms
- Tag enum + composition proof — covers all transitions simultaneously
This makes Verus independently satisfy the same proof obligation as Kani and Creusot, each from a different verification path.
Real invariant bodies are now in verus_gen.rs and emitted into
crates/elicitation_verus/src/generated/*.rs:
archive_panel_consistent:SqlEditor { running, result, .. }→running ==> result.is_None()archive_nav_consistent:NavFiltered { filter, .. }→filter@.len() > 0archive_overlay_consistent:ExportPickerOpen→idx <= formats@.len(),SavedBrowserOpen→idx <= entries@.len()archive_connection_consistent: triviallytrue(no cross-field constraints by design)
Phase 5 is implemented in strictly_games via the strictly_verus crate, not
vargo. The key insight is that the VerifiedStateMachine trait's new
vsm_verus_transitions() method generates fully self-contained Verus source
files — each file defines inline stub types and #[verifier::external] stubs
alongside assume_specification contracts.
Results: blackjack 11 verified, craps 8 verified, tictactoe 9 verified
— all with 0 errors. Run via just verify-verus-vsm in strictly_games.