Skip to content

Formal verification

Tests show behavior on the inputs they try. Two parts of Kiroha are verified more strongly: the concurrency and transaction state machines are model-checked in TLA+ over every interleaving of a bounded model, and the exact-decimal and JSON cores are checked with property-based tests against independent oracles. Each verified property was also shown to catch a deliberately injected bug ("mutation check"), so none is vacuously true.

Area Technique Where How to run
parallel scheduling (§9.1) TLA+ / TLC model checking formal/Parallel.tla ./formal/check.sh
tx lifecycle (§11.5) TLA+ / TLC model checking formal/Tx.tla ./formal/check.sh
Decimals (§3.2–3.3), JSON (§3.4–3.5) proptest with oracles tests/properties.rs cargo test --release --test properties

TLA+ models

The models are abstractions written to mirror the implementation's transition code; the correspondence is maintained by hand (comments in each module name the Rust functions it models). They verify the design of those state machines for every interleaving; they do not mechanically prove the Rust code.

Parallel.tla — xs.parallel(n){f}

Mirrors create_group / start_child / child_done in src/runtime/mod.rs. Each child may succeed, fail, or hold forever; an ancestor abort may arrive at any time.

Property Meaning
BoundedActive At most n callbacks are active, counting ones being cancelled
OrderedStarts Starts go to ascending indices; no index starts twice
NoStartAfterStop Nothing starts after a failure is latched or an abort is requested
JoinBeforeResume The parent resumes only after every started child is joined
CompleteValue A normal result means every item succeeded
ErrorIsLatched The raised error is the first observed failure; an ancestor abort dominates
EventuallyResumes (liveness) If no child holds forever, the parent eventually resumes

Checked with N = 5 items and n = 3: 1,212 distinct states, no violation.

Tx.tla — tx(d){f}

Mirrors TxFrame (tx_effect, tx_ret, tx_throw, tx_abort, tx_finish) in src/builtins/db.rs. The callback may run failing statements (which latch even if caught), return, or throw; a tx timeout or ancestor abort may arrive in any non-cleanup phase; COMMIT may be confirmed, rejected or lose its acknowledgment; ROLLBACK may succeed or fail.

Property Meaning
NeverCommitPoisoned A transaction with a failed sql(t) is never committed
NoRollbackAfterCommit ROLLBACK is never sent once COMMIT was dispatched
NoReplay The callback runs at most once
ConnectionAccounted Every acquired connection ends reset or discarded
UncertainDiscards A lost or unconfirmed COMMIT discards the connection
NeverReuseDirty A connection is reset for reuse only in a known ended state
Precedence Ancestor abort > tx timeout > first SQL failure > other callback failure
ValueMeansCommitted A value is returned only after a confirmed COMMIT
HonestTimeoutOutcome A tx timeout's outcome never claims more than is known (unknown once COMMIT was sent; rolled_back only after a confirmed rollback)
Completes (liveness) Once the callback has finished, the transaction completes

90 distinct states, no violation.

Mutation checks

Each injected defect is caught by the property named:

Model Injected defect Caught by
Parallel Keep starting children after a failure NoStartAfterStop
Parallel Resume the parent before joining children JoinBeforeResume
Tx Commit even when poisoned NeverCommitPoisoned
Tx Roll back after COMMIT was dispatched NoRollbackAfterCommit
Tx Reuse a connection after a lost COMMIT UncertainDiscards
Tx Report rolled_back for a timeout during COMMIT HonestTimeoutOutcome
Tx Let a SQL failure outrank a tx timeout Precedence
Tx Re-run the callback after a lost COMMIT NoReplay

Two of these initially went undetected, which exposed observability gaps in the first version of Tx.tla (a history variable recorded at the wrong time, and rollbackSent tied to the previous phase); the model was corrected until every mutant was caught.

Property-based tests

tests/properties.rs generates random decimals with up to 60 significant digits and exponents in ±40, and random JSON documents (nested arrays and records, control characters, quotes, backslashes, non-ASCII and astral characters in keys and strings). Each property runs 1,500–2,000 cases.

Property Oracle
Normalized form (coefficient not divisible by 10, zero is 0e0) Definition (§3.2)
Canonical spelling parses back to the same value; no +, no -0, no trailing fractional zero parse_json round trip (§3.3)
+, -, * exact; (a+b)-b = a Integer arithmetic at a common exponent
Comparison exact and antisymmetric Integer comparison at a common exponent
/ within half a unit of the 34th digit; ties to even Cross-multiplied integer error bound
Ties constructed deliberately round to even Expected 34-digit prefix computed directly
Quotients that fit in 34 digits are exact (q·b)/b = q
% equals a - trunc(a/b)·b, |r| < |b|, sign of a Integer truncating division
Zero divisors raise division_by_zero Definition
parse(dump(v)) = v; dump is idempotent; no raw control characters Round trip (§3.4)
Equality ignores record order, is symmetric; equal values hash equally Record-order reversal (§3.5)
Every truncation of a valid document is rejected (except numeric prefixes) Strict parser contract

Mutation checks: breaking ties-to-even is caught by the constructed-ties property (random inputs alone almost never hit an exact tie — that is why the constructed generator exists); taking the absolute value of remainders is caught by the remainder property.

Why not Kani or a proof assistant

Kani (bounded model checking of the Rust code itself) was tried first for the decimal module. It cannot analyse num-bigint, whose arithmetic uses inline assembly (_addcarry_u64), which Kani does not support; verifying a separate fixed-width re-implementation instead would not say anything about the real code. Mechanised proofs (Coq/Lean) of the whole semantics would cost far more than the rest of the project and still leave the code–proof correspondence open. The combination above targets the two areas where independent audits found the most defects.

Deterministic boundary regressions

tests/fixtures/cleanup.json checks one shared cleanup deadline across cancellation acknowledgments, rollback and release. All four cases reject the pre-fix implementation. tests/fixtures/durations.json exercises exact time representation and host clock-capacity aborts.

A Tokio paused-clock test in src/host/real/mod.rs advances 30 then 31 years through the production sleep adapter. Restoring the former 30-year clamp makes the test fail; restoring the checked full sleep makes it pass. This is a deterministic regression test, not a real 31-year observation or a TLA+ clock proof. The Linux I06 RNG-failure regression interposes the OS entropy call and verifies no predictable temporary file or replacement is created.