Skip to main content

Example Gallery

A curated collection of complete, runnable examples demonstrating Sector9 verification patterns. Each example is self-contained and can be verified independently.


Getting Started Examples​

Basic examples to understand core verification concepts.

Verified Counter​

The simplest verified actor: a counter with postconditions proving correctness.

counter.sr9
// Your first verified program
// A simple counter with a postcondition

persistent actor {
var count : Nat = 0;

// The `ensures` clause proves the return value equals count
public func get() : async Nat
reads count
ensures result == count;
{
count
};

// Proves that count increases by exactly 1
public func increment() : async Nat
modifies count
ensures count == old(count) + 1;
ensures result == count;
{
count += 1;
count
};
}

Key Points:

  • ensures result == count proves the return value matches the field
  • ensures count == old(count) + 1 proves increment adds exactly one
  • modifies count declares which fields change
  • reads count declares which fields are accessed

Token and Balance Systems​

Patterns for financial applications requiring conservation laws.

Token Conservation​

Track minting and burning with ghost variables to prove tokens are never created from nothing.

token-conservation-basic.sr9
// Basic token conservation pattern
persistent actor {
var balance : Nat = 0;
var totalMinted : Nat = 0;
var totalBurned : Nat = 0;

// Conservation invariant: minted tokens are either live or burned
invariant balance + totalBurned == totalMinted;

public func mint(amount : Nat) : async ()
modifies balance, totalMinted
reads totalBurned
entry_requires amount > 0;
requires amount > 0;
ensures balance == old(balance) + amount;
ensures totalMinted == old(totalMinted) + amount;
{
balance += amount;
totalMinted += amount;
};

public func burn(amount : Nat) : async ()
modifies balance, totalBurned
reads totalMinted
entry_requires amount > 0;
entry_requires amount <= balance;
requires amount > 0;
requires amount <= balance;
ensures balance == old(balance) - amount;
ensures totalBurned == old(totalBurned) + amount;
{
balance -= amount;
totalBurned += amount;
};
}

Key Points:

  • ghost var declares verification-only state (erased at runtime)
  • invariant balance == totalMinted - totalBurned enforces conservation
  • Ghost blocks (ghost { ... }) update ghost state alongside runtime state
  • The invariant is checked at every public function entry and exit

Multi-Account Token Transfer Model​

For systems with multiple accounts, keep deployable balances in runtime state and use ghost maps as proof models. The helper below is proof-only; it models the abstract map update for a transfer.

module {
public ghost func transferModel(
balances : Map<Nat, Nat>,
from : Nat,
to : Nat,
amount : Nat
) : Map<Nat, Nat>
requires from != to;
requires Map.getOr(balances, from, 0) >= amount;
ensures Map.getOr(result, from, 0) == Map.getOr(balances, from, 0) - amount;
ensures Map.getOr(result, to, 0) == Map.getOr(balances, to, 0) + amount;
{
let afterDebit = Map.update(balances, from, Map.getOr(balances, from, 0) - amount);
Map.update(afterDebit, to, Map.getOr(balances, to, 0) + amount)
};
}

Key Points:

  • Map<Nat, Nat> is a proof-only spec collection for key-value reasoning
  • Map.getOr provides a default for missing keys
  • Runtime ledgers should pair deployable storage with a ghost model

Access Control Patterns​

Patterns for authorization and permission checking.

Single Owner Pattern​

Restrict operations to a single owner with ownership transfer capability.

access-owner-pattern.sr9
// Single owner pattern with ownership transfer
persistent actor {
var balance : Nat = 0;
var owner : ?Principal = null;

// First caller claims ownership
public shared({caller}) func claimOwnership() : async ()
modifies owner
entry_requires owner == null;
requires owner == null;
ensures owner == ?caller;
{
owner := ?caller;
};

// Only owner can deposit
public shared({caller}) func deposit(amount : Nat) : async ()
modifies balance
reads owner
entry_requires owner == ?caller;
entry_requires amount > 0;
requires owner == ?caller;
requires amount > 0;
ensures balance == old(balance) + amount;
{
balance := balance + amount;
};

// Only owner can withdraw
public shared({caller}) func withdraw(amount : Nat) : async ()
modifies balance
reads owner
entry_requires owner == ?caller;
entry_requires amount <= balance;
requires owner == ?caller;
requires amount <= balance;
ensures balance == old(balance) - amount;
{
balance := balance - amount;
};

// Only owner can transfer ownership
public shared({caller}) func transferOwnership(newOwner : Principal) : async ()
modifies owner
entry_requires owner == ?caller;
requires owner == ?caller;
ensures owner == ?newOwner;
{
owner := ?newOwner;
};

// Anyone can check the owner
public func getOwner() : async ?Principal
reads owner
ensures result == owner;
{
owner
};
}

Key Points:

  • entry_requires owner == ?caller enforces owner-only entry at runtime
  • requires owner == ?caller gives the verifier the matching proof fact
  • Ownership transfer changes who can perform protected operations
  • Runtime state tracks authorization; ghost state can mirror proof models

Role-Based Access Control​

For more complex systems, use runtime role state for authorization. Ghost sets can mirror role membership for proofs, but they are not deployable access-control state.

persistent actor {
var admin : ?Principal = null;
var operator : ?Principal = null;

public shared({caller}) func claimAdmin() : async ()
modifies admin
entry_requires admin == null;
requires admin == null;
ensures admin == ?caller;
{
admin := ?caller;
};

public shared({caller}) func setOperator(account : Principal) : async ()
modifies operator
reads admin
entry_requires admin == ?caller;
requires admin == ?caller;
ensures operator == ?account;
{
operator := ?account;
};

public shared({caller}) func operatorAction() : async ()
reads operator
entry_requires operator == ?caller;
requires operator == ?caller;
{
// Operator-only action
};
}

Key Points:

  • entry_requires performs the deployable access check
  • Runtime fields or runtime collections store live roles
  • Ghost sets are useful for proofs about roles, not runtime authorization

State Machine Patterns​

Model valid state transitions with variant types and guards.

Order State Machine​

Enforce a linear progression through order states.

state-machine-basic.sr9
// Basic state machine with variant type and transition guards
persistent actor {
type OrderStatus = { #pending; #confirmed; #shipped; #delivered };

var status : OrderStatus = #pending;

public func confirm() : async ()
modifies status
entry_requires status == #pending;
requires status == #pending;
ensures status == #confirmed;
{
status := #confirmed
};

public func ship() : async ()
modifies status
entry_requires status == #confirmed;
requires status == #confirmed;
ensures status == #shipped;
{
status := #shipped
};

public func deliver() : async ()
modifies status
entry_requires status == #shipped;
requires status == #shipped;
ensures status == #delivered;
{
status := #delivered
};
}

Key Points:

  • Variant types ({ #pending; #confirmed; ... }) enumerate valid states
  • requires status == #pending ensures valid transitions
  • ensures status == #confirmed guarantees the new state
  • Invalid transitions (e.g., pending -> delivered) are statically rejected

State Machine with Payload​

Track additional data that depends on the current state.

persistent actor {
type State = {
#idle;
#active : { startTime : Nat };
#completed : { startTime : Nat; endTime : Nat };
};

var state : State = #idle;

invariant switch (state) {
case (#completed({ startTime; endTime })) { startTime <= endTime };
case (_) { true };
};

public func start(now : Nat) : async ()
modifies state
entry_requires state == #idle;
requires state == #idle;
ensures state == #active({ startTime = now });
{
state := #active({ startTime = now });
};

public func complete(now : Nat) : async ()
modifies state
entry_requires switch (state) { case (#active(_)) { true }; case (_) { false } };
entry_requires switch (state) { case (#active({ startTime })) { now >= startTime }; case (_) { false } };
requires switch (state) { case (#active(_)) { true }; case (_) { false } };
requires switch (state) { case (#active({ startTime })) { now >= startTime }; case (_) { false } };
{
switch (state) {
case (#active({ startTime })) {
state := #completed({ startTime; endTime = now });
};
case (_) { };
};
};
}

Key Points:

  • Payload data varies by state
  • Invariants can use pattern matching to express state-dependent properties
  • Preconditions extract and validate payload values

Ghost State Patterns​

Use ghost state for abstract reasoning about program behavior.

Shadow Variable​

Mirror runtime state with ghost state to establish relationships.

ghost-var-basic.sr9
// Basic ghost variable example

persistent actor {
// Runtime state
var counter : Nat = 0;

// Ghost state - exists only for verification
ghost var shadow : Nat = 0;

// Invariant relates ghost and runtime state
invariant counter == shadow;

public func increment() : async ()
modifies counter, shadow
ensures counter == old(counter) + 1;
{
counter += 1;
ghost { shadow := shadow + 1; };
};

public func get() : async Nat
reads counter
ensures result == counter;
{
counter
};
}

Key Points:

  • Ghost variables track abstract properties not needed at runtime
  • Invariants relate ghost and runtime state
  • Ghost updates must maintain invariants

History Tracking​

Record operation history for audit or verification purposes.

persistent actor {
var balance : Int = 0;
ghost var history : Seq<Int> = Seq.empty();

// History records all changes for proof/debugging.

public func deposit(amount : Int) : async ()
modifies balance, history
entry_requires amount > 0;
requires amount > 0;
ensures balance == old(balance) + amount;
{
balance += amount;
ghost {
history := Seq.append(history, amount);
};
};

public func withdraw(amount : Int) : async ()
modifies balance, history
entry_requires amount > 0;
entry_requires amount <= balance;
requires amount > 0;
requires amount <= balance;
ensures balance == old(balance) - amount;
{
balance -= amount;
ghost {
history := Seq.append(history, -amount);
};
};
}

Key Points:

  • Seq<Int> tracks ordered history of changes
  • Sum of history equals current balance (conservation)
  • History can support audit requirements in postconditions

Quantifier Patterns​

Express properties over collections and ranges.

Universal Quantification​

Prove a property holds for all values.

forall-basic.sr9
// Prove that addition is commutative for all natural numbers
persistent actor {
public func check() : async () {
ghost {
assert forall<Nat>(pure func (i : Nat) : Bool =
forall<Nat>(pure func (j : Nat) : Bool =
i + j == j + i));
};
};
}

Key Points:

  • forall<T>(pure func (x : T) : Bool = ...) expresses universal properties
  • Nested forall handles multi-variable properties
  • Quantified assertions guide the SMT solver

Array Range Properties​

Prove properties about array elements.

persistent actor {
var data : [var Nat] = [var];

// All elements are non-negative (trivially true for Nat, but shows pattern)
pure func allNonNegative(arr : [Nat]) : Bool {
forall<Nat>(pure func (i : Nat) : Bool =
i >= arr.size() or arr[i] >= 0)
};

// All elements at even indices are even
pure func evenAtEvenIndices(arr : [Nat]) : Bool {
forall<Nat>(pure func (i : Nat) : Bool =
i >= arr.size() or i % 2 != 0 or arr[i] % 2 == 0)
};

public func checkProperties() : async ()
reads data
ensures allNonNegative(Array.freeze(data));
{
// Verification proves the property holds
};
}

Key Points:

  • Pure functions encapsulate quantified properties
  • Guard with i >= arr.size() to handle out-of-bounds
  • Use ==> to create implications: i < size ==> property

Lemma Patterns​

Use lemmas to establish facts the verifier needs help proving.

Basic Lemma​

Establish arithmetic facts for use in verification.

lemma-basic.sr9
// Basic lemma declaration with preconditions and postconditions
module {
// A lemma establishing an arithmetic fact
public lemma addPositive(x : Nat, y : Nat) : ()
requires x > 0;
requires y > 0;
ensures x + y > x;
ensures x + y > y;
{
// Body can be empty - verifier proves postconditions from preconditions
};

// Using the lemma to help verify a function
public func sumPositive(a : Nat, b : Nat) : Nat
requires a > 0;
requires b > 0;
ensures result > a;
ensures result > b;
{
addPositive(a, b); // Call lemma to establish facts
a + b
};
}

Key Points:

  • Lemmas have requires and ensures like functions
  • Bodies can be empty if the verifier proves postconditions from preconditions
  • Call lemmas to inject their postconditions as known facts

Step Lemma for Induction​

Break complex proofs into steps.

module {
// Step lemma: if n squares are at most n^2, so is n+1
public lemma squareStep(n : Nat) : ()
requires n > 0;
ensures (n + 1) * (n + 1) == n * n + 2 * n + 1;
{
// Algebraic identity - verifier can prove this
};

// Use the step lemma in a loop
public func sumOfSquares(limit : Nat) : Nat
ensures result <= limit * limit * limit;
{
var sum : Nat = 0;
var i : Nat = 0;

while (i < limit) {
loop:invariant i <= limit;
loop:invariant sum <= i * i * i;
if (i > 0) { squareStep(i); };
sum += i * i;
i += 1;
};
sum
};
}

Key Points:

  • Step lemmas help with inductive arguments
  • Call the lemma where its facts are needed
  • Loop invariants build on lemma-established facts

Spec Collection Patterns​

Use Map, Set, Seq, and Multiset for abstract reasoning.

Map Operations​

Key-value storage with functional updates.

map-basic.sr9
// Basic Map operations: empty, update, get, contains
persistent actor {
public func demo() : async () {
ghost {
// Create an empty Map
let m0 : Map<Nat, Int> = Map.empty();

// Update adds a key-value pair, returns a new Map
let m1 = Map.update(m0, 1, 10);
let m2 = Map.update(m1, 2, 20);

// Check membership
assert Map.contains(1, m2);
assert Map.contains(2, m2);
assert not Map.contains(3, m2);

// Get values
assert Map.get(m2, 1) == 10;
assert Map.get(m2, 2) == 20;
};
};
}

Key Points:

  • Map.empty() creates an empty map
  • Map.update(m, k, v) returns a new map with the binding
  • Map.get(m, k) retrieves a value
  • Map.contains(k, m) checks membership

Set Operations​

persistent actor {
public func setDemo() : async () {
ghost {
let s0 : Set<Nat> = Set.empty();
let s1 = Set.union(s0, Set.singleton(1));
let s2 = Set.union(s1, Set.singleton(2));
let s3 = Set.union(s2, Set.singleton(3));

// Membership
assert Set.contains(1, s3);
assert Set.contains(2, s3);
assert not Set.contains(4, s3);

// Cardinality
assert Set.card(s3) == 3;

// Set operations
let s4 = Set.union(Set.union(Set.empty(), Set.singleton(2)), Set.singleton(4));
let inter = Set.intersect(s3, s4); // {2}
let union = Set.union(s3, s4); // {1, 2, 3, 4}

assert Set.contains(2, inter);
assert not Set.contains(1, inter);
assert Set.card(union) == 4;
};
};
}

Sequence Operations​

persistent actor {
public func seqDemo() : async () {
ghost {
let s0 : Seq<Nat> = Seq.empty();
let s1 = Seq.append(s0, 1);
let s2 = Seq.append(s1, 2);
let s3 = Seq.append(s2, 3);

// Length and indexing
assert Seq.len(s3) == 3;
assert Seq.get(s3, 0) == 1;
assert Seq.get(s3, 2) == 3;

// Slicing
let slice = Seq.slice(s3, 1, 3); // [2, 3]
assert Seq.len(slice) == 2;

// Concatenation
let s4 = Seq.concat(s3, s3); // [1, 2, 3, 1, 2, 3]
assert Seq.len(s4) == 6;
};
};
}

Loop Invariant Patterns​

Verify loops with inductive invariants.

Array Processing Loop​

Track progress through an array with quantified invariants.

persistent actor {
public func doubleAll(data : [var Nat]) : async ()
modifies data
ensures forall<Nat>(pure func (i : Nat) : Bool =
i >= data.size() or data[i] == old(data[i]) * 2);
{
var idx : Nat = 0;

while (idx < data.size()) {
loop:invariant idx <= data.size();
// Processed elements are doubled
loop:invariant forall<Nat>(pure func (j : Nat) : Bool =
j >= idx or data[j] == old(data[j]) * 2);
// Unprocessed elements are unchanged
loop:invariant forall<Nat>(pure func (j : Nat) : Bool =
j < idx or j >= data.size() or data[j] == old(data[j]));
data[idx] := data[idx] * 2;
idx += 1;
};
};
}

Key Points:

  • loop:invariant annotates while loops
  • Track both processed and unprocessed regions
  • Bounds invariant (idx <= data.size()) prevents overflow

Accumulator Loop​

Track accumulated values.

persistent actor {
public func count(data : [Nat]) : async Nat
ensures result == data.size();
{
var total : Nat = 0;
var idx : Nat = 0;

while (idx < data.size()) {
loop:invariant idx <= data.size();
loop:invariant total == idx;
total += 1;
idx += 1;
};
total
};
}

Async Verification Patterns​

Handle verification across await points.

Invariant Preservation Across Await​

Actor invariants must hold before and after await.

persistent actor {
var balance : Nat = 0;
var locked : Bool = false;

invariant not locked ==> balance >= 0;

public func processPayment(amount : Nat) : async ()
modifies balance, locked
entry_requires not locked and balance >= amount;
requires not locked;
requires balance >= amount;
{
locked := true;

// Before await: invariant must hold
// locked = true, so invariant is trivially satisfied

try { await someExternalCall(); } catch (_) {
locked := false;
return;
};

// After await: invariant must be re-established
// External code may have run; recheck state before using it
if (balance < amount) {
locked := false;
return;
};

balance -= amount;
locked := false;
// Now: not locked and balance >= 0
};

func someExternalCall() : async () { };
}

Key Points:

  • Invariants are checked at await boundaries
  • Design invariants that remain true even after interference
  • Use lock flags to weaken invariants during critical sections

Pure Function Patterns​

Side-effect-free functions usable in specifications.

Pure Predicates​

Define reusable conditions for contracts.

module Validation {
public pure func isValidAmount(amount : Nat) : Bool {
amount > 0 and amount <= 1_000_000
};

public pure func isValidAccount(account : Nat) : Bool {
account > 0
};
}

persistent actor {
var balance : Nat = 0;

public func deposit(account : Nat, amount : Nat) : async ()
modifies balance
entry_requires Validation.isValidAmount(amount);
entry_requires Validation.isValidAccount(account);
requires Validation.isValidAmount(amount);
requires Validation.isValidAccount(account);
ensures balance == old(balance) + amount;
{
balance += amount;
};
}

Key Points:

  • Pure functions cannot mutate state or await
  • They can appear in requires, ensures, and invariant
  • Extract complex conditions into named predicates for clarity

Common Mistakes and How to Fix Them​

Missing Modifies Clause​

// Wrong: modifies clause missing
public func increment() : async ()
ensures count == old(count) + 1; // Error: cannot write to count
{
count += 1;
}

// Correct: declare what you modify
public func increment() : async ()
modifies count
ensures count == old(count) + 1;
{
count += 1;
}

Unguarded Division​

// Wrong: division by zero possible
public func average(total : Nat, count : Nat) : async Nat
ensures result == total / count;
{
total / count
}

// Correct: explicit non-zero guard
public func average(total : Nat, count : Nat) : async Nat
entry_requires count > 0;
requires count > 0;
ensures result == total / count;
{
total / count
}

Non-Inductive Loop Invariant​

// Wrong: invariant not preserved by loop body
while (i < n)
loop:invariant sum == i * i; // Not true after i += 1
{
sum += 2 * i + 1;
i += 1;
}

// Correct: invariant holds before and after each iteration
while (i < n)
loop:invariant sum == i * i; // (i+1)^2 = i^2 + 2i + 1
{
sum += 2 * i + 1; // sum becomes (i+1)^2
i += 1; // i becomes i+1, invariant restored
}

Summary​

This gallery demonstrates the core patterns for Sector9 verification:

PatternKey ConstructsUse Case
Token Conservationghost var, invariantFinancial correctness
Access Controlentry_requires owner == ?caller plus requires owner == ?callerAuthorization
State MachineVariant types, transition guardsProtocol states
Ghost Stateghost { }, shadow variablesAbstract properties
Quantifiersforall, existsCollection properties
Lemmaslemma, step functionsComplex proofs
Spec CollectionsMap, Set, SeqAbstract data
Loop Invariantsloop:invariantIterative algorithms
Async SafetyInvariants at awaitConcurrency
Pure Functionspure funcReusable predicates

For more details on any pattern, see the corresponding section in the main documentation.