◇ OpenCyber Academy
Engineering · Chapter 9 · English

Chapter 9 — Programming II

Open, free learning for anyone who wants to protect people and technology. No account is required to read this text or its worked answers.

Chapter 9 — Programming II

Lesson map

Lesson 09 published. Map, example, EX017.

flowchart TD
  N0["Learning outcomes and connection"]
  N1["9.1 Modules, interfaces and invariants"]
  N2["9.2 State machines"]
  N3["9.3 Testing"]
  N4["9.4 Errors, time and concurrency"]
  N5["9.6 Practical activity"]
  N6["Chapter synthesis"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6
  N6 --> EX["Worked example"]
  EX --> Q["Exercise EX017"]

Logical drawing

flowchart LR
  N0["Learning outcomes and connection"]
  N1["9.1 Modules, interfaces and invariants"]
  N2["9.2 State machines"]
  N3["9.3 Testing"]
  N4["9.4 Errors, time and concurrency"]
  N5["9.6 Practical activity"]
  N6["Chapter synthesis"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> Q["EX017"]
[Learning outcomes and connection]
    |
    v
[9.1 Modules, interfaces and invariants]
    |
    v
[9.2 State machines]
    |
    v
[9.3 Testing]
    |
    v
[9.4 Errors, time and concurrency]
    |
    v
[9.6 Practical activity]
    |
    v
[Chapter synthesis]
    |
    v
[Worked example] --> [Exercise EX017]

Learning outcomes and connection

After this chapter you can (1) structure a program into modules with clear interfaces and invariants; (2) implement simple state machines for protocol and equipment logic; (3) write unit tests, including boundary and failure tests, and measure what they cover; and (4) handle errors, time and concurrency deliberately. It builds on Chapter 4 and prepares Chapter 14 (software engineering) and Chapter 30 (verification).

9.1 Modules, interfaces and invariants

Parnas (1972) argued that a system should be decomposed so that each module hides a design decision likely to change. In infrastructure software, likely changes include the field protocol, the historian, and the authentication scheme. Hide each behind an interface: a TelemetrySource that yields validated readings, whatever the protocol. Each module should state its invariants — properties always true of its state — for example, "the last accepted counter for each station never decreases". Write invariants as assertions in tests and, where cheap, as runtime checks that fail safe.

9.2 State machines

Equipment and protocols have modes: a pump is stopped, starting, running, stopping or tripped; a session is unauthenticated, authenticated or closed. A finite state machine lists states, the events that cause transitions, guards on transitions and actions. Write it as a table and implement it from the table, rejecting any event not listed for the current state. Unlisted transitions are where both bugs and attacks live: a "stop" command arriving while starting, or a "write setpoint" arriving before authentication.

LRWA's pump controller in the worked example below is built exactly this way, so its tests can enumerate every pair.

9.3 Testing

A unit test checks one behaviour of one unit. Good test suites include: typical cases; boundaries (minimum, maximum, just inside, just outside); invalid input; and regression tests reproducing past bugs. Coverage measures which statements or branches the tests executed; it tells you what was not tested, not that the tested code is correct. Python's unittest module, in the standard library, is enough for this book.

For LRWA's pump controller, boundary tests include the 30-second start timeout at exactly 30 seconds and just beyond it, and regression tests reproduce every transition bug found in commissioning, so a fixed fault cannot quietly return in a later release.

9.4 Errors, time and concurrency

Decide how each error is handled: rejected with a reason, retried with a limit, or escalated. Never swallow exceptions silently in control or security code. Time is a recurring source of failure: use monotonic clocks for intervals and synchronised wall-clock time (with its source recorded) for logs; handle clock steps. Concurrency — threads, asynchronous tasks, multiple clients — creates race conditions: two operators issuing conflicting setpoints, or a check and an action separated by a window in which state changes (time-of-check to time-of-use). Prefer a single owner for each piece of state, with messages to change it.

9.5 Worked example: a pump state machine (synthetic)

LRWA's pump controller accepts start, stop, trip and reset. The table: stopped + start (guard: permissive true) → starting; starting + run_confirmed → running; starting + timeout 30 s → tripped; running + stop → stopping; any state + trip → tripped; tripped + reset (guard: local operator) → stopped. The guard on reset matters: requiring a local action after a trip means a remote attacker who causes a trip cannot immediately restart the pump into the same fault. Tests cover every listed transition and assert that every unlisted pair (for example tripped + start) is rejected and logged.

9.6 Practical activity

Implement the table in §9.5 in about 40 lines of Python with a dictionary keyed by (state, event). Write unittest cases for all listed transitions and a loop that asserts every unlisted (state, event) pair raises an error. Count the unlisted pairs.

Your loop should print the number of unlisted pairs and confirm that each one raises an error and writes a log line. Then add a test showing that a remote reset after a trip is refused, because the guard requires a local operator. That test protects the property the worked example cares about most: an attacker who trips a pump remotely cannot restart it straight into the same fault. Keep the table and the tests in one file so a reviewer can compare them.

9.7 Common errors and limits

Each error defeats a property of the pump state machine. Scattered flags make it impossible to list every transition. A default branch would let a start command through while the pump is tripped. A wall-clock timeout could fire early or never after a clock step, and an exception swallowed in the reset path would leave the pump in an unknown mode.

Exercises — chapter 9

EX017 — Count and test the unlisted transitions. Objective: reason about completeness. Inputs: §9.5 (states: stopped, starting, running, stopping, tripped; events: start, stop, trip, reset, run_confirmed, timeout, stop_confirmed — add stopping + stop_confirmed → stopped). Steps: count total pairs, listed pairs and unlisted pairs; give three unlisted pairs whose rejection has a safety or security rationale. Expected evidence: counts and rationale. Rubric: 3 = correct counts and meaningful rationales; 2 = count error; 1 = no rationale; 0 = none.

EX018 — Replay invariant. Objective: express and test an invariant. Inputs: Lab 8's Receiver. Steps: write the invariant on last, a unit test that sends counters 1, 2, 2, 1, 5 and asserts accept/reject results and the final state, and state one failure mode of the invariant after a receiver restart. Expected evidence: test code and statement. Rubric: 3 = correct expected results (accept, accept, reject, reject, accept), restart issue identified (state lost → replays accepted unless persisted or keys rotated); 2 = restart missed; 1 = untested; 0 = none.

Chapter synthesis

Decompose around decisions likely to change and state invariants. Make modes explicit as state machines and reject everything not listed. Test boundaries and failures, read coverage as a map of the untested, handle errors deliberately, use the right clock, and give each piece of shared state one owner.

Continue studying — chapter 09 of 50

← Chapter 08 · Course chapter index · Chapter 10 →

Continue to the next authored lesson.