Chapters

Case Study: State Transitions and Invariants

Establish safety using the same definitions that drive the program.

19.1 Put updates in one function

To reason about the safety of state updates, make the possible states and operations explicit. Consider a gate that authorizes or revokes permission to spend.

src/ch19-gate.taut
import std::Nat

inductive GateState: Type {
  GateClosed(balance: Nat): GateState
  GateOpen(balance: Nat): GateState
}

inductive GateCommand: Type {
  GateAuthorize: GateCommand
  GateRevoke: GateCommand
  GateSpend: GateCommand
}

fn gate_balance(state: GateState): Nat {
  match state: Nat {
    GateClosed(n) => n
    GateOpen(n) => n
  }
}

// Represent authorization and the balance explicitly in the state.
✓ Check passed
✓ inductive GateState : Type
✓ inductive GateCommand : Type
✓ def gate_balance : (GateState) -> Nat

GateOpen permits spending; GateClosed does not. A Nat represents the balance.

19.2 Make the transition the specification

src/ch19-gate.taut
fn gate_step(state: GateState, cmd: GateCommand): GateState {
  match cmd: GateState {
    GateAuthorize => GateOpen(gate_balance(state))
    GateRevoke => GateClosed(gate_balance(state))
    GateSpend => match state: GateState {
      GateClosed(n) => GateClosed(n)
      GateOpen(n) => GateOpen(Nat::pred(n))
    }
  }
}

// Keep all state changes in the same pure transition function.
✓ Check passed
✓ def gate_step : (GateState) -> (GateCommand) -> GateState

GateSpend leaves a closed gate unchanged. If the gate is open, it reduces the balance by one. Nat::pred operates on natural numbers, so its behavior at zero is defined too.

19.3 Declare an invariant

src/ch19-gate.taut
def GateInvariant(capacity: Nat, state: GateState): Prop :=
  gate_balance(state) <= capacity = true

// State that the balance never exceeds the supplied capacity.
✓ Check passed
✓ def GateInvariant : (Nat) -> (GateState) -> Prop

This proposition says that the current balance is at most the initial capacity: the Boolean comparison must equal true. Prove preservation by each operation, and you can establish preservation over any finite sequence of operations.

The property that GateRevoke always closes the gate follows by computation alone.

src/ch19-gate.taut
import std::Base

theorem revoke_closes(s: GateState): gate_step(s, GateRevoke) = GateClosed(gate_balance(s)) {
  Refl
}

// Computation shows that revocation always produces a closed gate.
✓ Check passed
✓ theorem revoke_closes : ∀ s: GateState. gate_step(s, GateRevoke) = GateClosed(gate_balance(s))

19.4 Execute the transition you proved

Proving a pure transition does not guarantee an application that uses a separately written, unproved update function. Design the runtime to call the same gate_step that the proof concerns. The repository's examples/certified-state/ demonstrates this arrangement.

Adapters that assign to external mutable cells, extraction, and native runtimes remain separate parts of the trusted system. A mathematical proof of a transition is not the same as mechanical verification of all those components.

Exercise 19.1: What parameters and result type would express that spending at a closed gate leaves the entire state unchanged?