Skip to main content

Modular Verification

Modular verification means verifying components independently rather than as a monolithic whole. This improves verification performance by reducing the scope of each verification task and enables parallel verification of separate modules.

Why Modular Verification Matters​

Large actors with many methods can cause verification timeouts. The SMT solver must reason about state, contracts, and interactions for each root it verifies. Modular verification addresses this by:

  1. Reducing verification scope: Each module is verified against its contracts, not its callers
  2. Enabling parallelism: Independent modules can be verified concurrently
  3. Improving cacheability: Unchanged modules don't need re-verification
  4. Isolating failures: Contract violations localize to specific boundaries
ApproachVerification TimeParallelizableFailure Isolation
Monolithic actorSlowNoPoor
Extracted pure helpersFasterYesGood
Abstract proof-only stubsFastestYesExcellent

The Contract Boundary Principle​

Modular verification works because the verifier treats function contracts as complete specifications. When verifying a caller:

  1. Preconditions are proof obligations at call sites
  2. Postconditions are assumptions about the result
  3. The callee body is not inlined into the caller's proof

This separation means Module A can rely on Module B's exported contracts instead of re-proving Module B's body at every call site. The imported module still needs to type-check, and you should verify it separately before treating the boundary as established.

Pattern 1: Signature-Only Declarations​

Declare ghost abstract functions with contracts but no body. The verifier treats these as temporary specifications for proof-only code:

modular-signature-only.sr9
// Abstract signature-only declarations enable modular verification
// The verifier uses contracts without requiring implementations

module {
// Abstract signature-only: contract specified, no body
public ghost abstract func abs(x : Int) : Nat
requires true;
ensures result >= 0;
ensures x >= 0 ==> result == x;
ensures x < 0 ==> result == -x;

// Proof-only code can use abs() via its contract.
public ghost func distance(a : Int, b : Int) : Nat
ensures result >= 0;
{
abs(a - b)
};
}

How it works: The abs function is marked ghost abstract and has no body. The verifier generates an uninterpreted Viper function with the specified contracts. When the proof-only distance helper calls abs, it:

  • Must establish any preconditions (none here)
  • May assume the postconditions hold for the result

When to use:

  • Verification-Driven Development for ghost helpers and lemmas
  • External mathematical facts you trust but don't want to prove yet
  • Breaking circular dependencies in proof-only code

Signature-only declarations are not runtime implementations. Runtime code must call functions with bodies; keep declaration-only helpers in ghost/proof-only code. Use abstract for verification-driven placeholders and trusted only for permanent assumptions.

Pattern 2: Trusted Functions​

Mark functions as trusted to skip body verification while keeping contracts:

modular-trusted-boundary.sr9
// Trusted functions create verification boundaries
// The body is not verified; contracts are assumed

module {
// Trusted: body not verified, postcondition assumed
public trusted func fastHash(data : Nat) : Nat
requires data < 1_000_000;
ensures result < 256;
{
// Complex implementation the verifier skips
data % 256
};

// Verified caller: must establish precondition
public func hashIfSmall(n : Nat) : ?Nat
ensures result == null or n < 1_000_000;
{
if (n < 1_000_000) {
?fastHash(n) // Precondition satisfied
} else {
null
}
};
}

How it works: The trusted modifier tells the verifier:

  • Assume the postcondition result < 256 holds
  • Skip verification that the body actually achieves this
  • Verify that callers satisfy the precondition data < 1_000_000

When to use:

  • Complex algorithms that are correct but hard to verify
  • Performance-critical code with hand-optimized implementations
  • Third-party code you've audited but can't modify

Soundness warning: Trusted functions are assumptions. If the body doesn't actually satisfy the contract, callers may rely on false guarantees. Keep the trusted surface small and auditable; see the trusted computing base design notes.

Pattern 3: Import Chains​

Imported modules give callers a contract boundary. Callers reason from exported contracts, not implementation details:

modular-mathlib.sr9
// Library module verified independently
// Callers rely on contracts, not implementation details

module MathLib {
public pure func clamp(x : Nat, lo : Nat, hi : Nat) : Nat
requires lo <= hi;
ensures result >= lo;
ensures result <= hi;
ensures x < lo ==> result == lo;
ensures x > hi ==> result == hi;
ensures x >= lo and x <= hi ==> result == x;
{
if (x < lo) { lo }
else if (x > hi) { hi }
else { x }
};

public pure func max(a : Nat, b : Nat) : Nat
ensures result >= a;
ensures result >= b;
ensures result == a or result == b;
{
if (a >= b) { a } else { b }
};
}

How it works: When verifying TokenActor:

  1. MathLib.clamp's body is not inlined into the actor method proof
  2. Only the contract is used: ensures result <= hi
  3. The caller's proof uses this contract to establish result <= max

When to use: Always structure code this way. Separate concerns into modules with clear contracts.

Pattern 4: Verification-Driven Development​

Write proof-helper contracts first, verify ghost callers against stubs, implement later:

modular-vdd-stubs.sr9
// Verification-Driven Development for proof helpers: stubs first, implementation later
// Write contracts, verify proof-only callers, then implement

module {
// Stage 1: Abstract ghost helper stubs with contracts only
public ghost abstract func lineTotal(qty : Nat, price : Nat) : Nat
requires true;
ensures result == qty * price;

public ghost abstract func applyDiscount(amount : Nat, rate : Nat) : Nat
requires rate <= 100;
ensures result <= amount;
ensures result == amount - (amount * rate / 100);

// Stage 2: Verified proof-only function using stubs
public ghost func computeInvoice(qty : Nat, price : Nat, discountRate : Nat) : Nat
requires discountRate <= 100;
ensures result <= qty * price;
{
let subtotal = lineTotal(qty, price);
applyDiscount(subtotal, discountRate)
};

// Stage 3: Implement stubs later (not shown)
// The caller verification already passed using contracts
}

The VDD workflow:

  1. Declare stubs: Abstract ghost functions with contracts, no bodies
  2. Write ghost callers: Proof-only functions or lemmas that use the stubs
  3. Verify callers: They verify against stub contracts
  4. Implement stubs: Fill in bodies that satisfy contracts
  5. Verify stubs: Confirm implementations meet contracts

Why this helps: You can verify the proof design before committing to implementations. Runtime callers still need helpers with bodies.

Pattern 5: Extract Pure Helpers​

Move complex logic into pure functions that can be verified independently:

modular-split-monolith.sr9
// Before: Monolithic actor with slow verification
// After: Extracted pure helpers enable parallel verification

module TokenMath {
// Verified independently - fast
public pure func validateTransfer(balance : Nat, amount : Nat) : Bool
ensures result == (amount <= balance);
{
amount <= balance
};

public pure func computeFee(amount : Nat, basisPoints : Nat) : Nat
requires basisPoints <= 10000;
ensures result <= amount;
{
amount * basisPoints / 10000
};
};

persistent actor TokenActor {
var balances : [var Nat] = [var];

// Actor methods use pure helpers
// Verification leverages helper contracts
public func transfer(from : Nat, to : Nat, amount : Nat) : async Bool
modifies balances
entry_requires from < balances.size() and to < balances.size();
requires from < balances.size();
requires to < balances.size();
{
if (TokenMath.validateTransfer(balances[from], amount)) {
balances[from] -= amount;
balances[to] += amount;
true
} else {
false
}
};
}

How it works:

  • TokenMath module contains pure helpers with contracts
  • TokenActor uses these helpers, relying on their contracts
  • Each component verifies faster than the combined whole

Splitting guidelines:

  • Extract validation logic into pure predicates
  • Move calculations into pure functions
  • Keep actor methods focused on state transitions

Diagnosing Modular Verification Opportunities​

Signs you need modularization:

  • Verification times out on large actors
  • Adding unrelated methods slows down existing verification
  • Similar logic is duplicated with slight variations

Check verification times:

time XDG_CACHE_HOME=/tmp/sector9 ./bin/sector9 --verify large-actor.mo

If verification takes more than 30 seconds, consider extracting modules.

Identify hot spots:

XDG_CACHE_HOME=/tmp/sector9 ./bin/sector9 --viper --deterministic large-actor.mo > output.vpr

Look for methods with many assertions, quantifiers, or array operations. These are extraction candidates.

Performance Comparison​

BeforeAfterImprovement
1 actor, 20 methods, 45s4 modules, 60s total but parallel4x with parallel verification
1 file with 10 helpersHelpers in separate moduleEach module verifies in <10s
Trusted inline codeExplicit trusted boundaryClear soundness scope

Modular Verification Checklist​

  1. Extract pure helpers - Move calculations and predicates to separate modules
  2. Use abstract signature-only stubs only in proof code - Runtime code needs helpers with bodies
  3. Mark trusted code explicitly - Don't hide assumptions in implementations
  4. Verify imports independently - Each module should verify on its own
  5. Minimize cross-module state - Prefer immutable data passing over shared state
  6. Document contract boundaries - Make it clear what each module guarantees
  7. Test modules in isolation - Verification tests should cover each module separately

See Also​

Summary​

  • Modular verification breaks large verification tasks into independent components
  • Abstract signature-only declarations let you verify proof-only callers before implementing functions
  • Trusted functions create explicit boundaries where the verifier assumes contracts
  • Import chains verify each module independently against its contracts
  • Verification-Driven Development uses abstract proof-only stubs to validate proof design before implementation
  • Extracting pure helpers enables parallel verification and faster iteration
  • Contract boundaries are the key mechanism: callers use contracts, not implementations