FoundationDB famously built its testing strategy on a deterministic simulator: run the whole distributed database inside one controlled process, seed every source of randomness, and replay any failure from its recorded inputs. It is one of the most admired pieces of testing infrastructure ever built, and it is out of reach for a small project, because it means writing your system twice: once for the world and once for the simulator.
Platonik’s Rust engine reaches for the same guarantees at a fraction of the budget, and it does so by a simpler route: the world is small enough that the real implementation can be deterministic, not a simulation of it. There is no second model; there is one integer-only engine, a declared seed, and a verification rule that treats re-execution as the only acceptable proof. This article walks through how that works in crates/platonik-core/src/sim.rs and the CLI’s journal, and where the discipline’s limits honestly lie.
what deterministic simulation is for
The reason FoundationDB simulated at all was that distributed systems are hostile to testing: failures live in timing, interleaving, and rare event sequences you cannot reproduce by rerunning a unit test. A deterministic simulator collapses the space: if the run is a pure function of its inputs, every failure is a reproducible artifact, and “we saw it once” becomes “here is the trace.”
Platonik needs the same property for a different reason. Its runs are evidence: a courier recovering from a closed route, a keeper retaining a report through an outage, a constructed child doing useful work. Each claim is only as strong as the replay that backs it. If the engine admitted hidden randomness, wall-clock dependence, or ambient state, a receipt would be a screenshot: plausible, unverifiable. The requirement flows from the product, not from testing hygiene: “a submitted organism must function when the scientist is disconnected,” and a result must function when the original machine is gone.
one seed, no clock
The determinism budget is spent where nondeterminism actually enters: scheduling, arithmetic, and external input. Each gets a structural answer.
Scheduling. The one genuinely nondeterministic-looking choice, which cell acts when, is made a pure function. order_ids sorts eligible cell ids by mix(seed ^ (tick << 32) ^ id), where mix is the SplitMix64 finalizer; ties break on the raw id. The schedule varies by tick (no fixed first mover) yet is entirely recomputable from the experiment’s declared seed: u64. Everything else in a tick is fixed order: expire unread inboxes, apply declared events, deliver due signals, activate each cell once, drain beacons, then charge checking and audit invariants.
Arithmetic. The model is integer-only (u8 registers, u16 ids, u32 charges, u64 work), so there is no floating-point to diverge across architectures, no NaN to sneak a platform difference into a hash. Release builds keep overflow-checks = true (Cargo.toml), turning a wraparound bug into a panic rather than a platform-dependent value. The toolchain itself is an input: rust-toolchain.toml pins Rust 1.97.1, and Cargo.lock plus exact = version pins on serde, serde_json, and sha2 make the dependency graph part of the declared semantics.
Input. An Experiment is the complete declared input (world, seed, programs, events, budgets), capped at 64 KiB of canonical JSON. There is no ambient authority: no filesystem read mid-run, no network, no clock, no evaluator identity visible to a program. The process-local EXECUTIONS counter in sim.rs is documented as observer telemetry outside the deterministic receipt, the one deliberate exception, kept outside the hashed artifact precisely because it would differ across replays.
atomic steps and live invariants
Determinism is about reproducibility; atomicity is about never having to reproduce a torn state. Each cell activation clones the world State, executes into the clone, and commits on success or ordinary action failure; a fuel or activation-limit fault discards the clone while the shared meter keeps the spent work. Later cells in the sweep see only committed states: there is no observable half-action, and “movement never grants an extra activation” is a property of the commit structure, not a policy.
The checker does not wait for the run to end. At the close of every tick, check_live_invariants re-derives the world’s conservation laws from the live state: every spark id exists in exactly one place (a source, a depot, a cargo slot, or the delivery log) with its original bit; each beacon’s charge + drained equals initial_charge + deliveries × spark_charge; closed edges are a sorted subset of declared edge_blocked events; no two cells occupy a tile; exhaustion is recorded when charge hits zero. A violation returns an error mid-run rather than being discovered in a postmortem: the invariants are part of the semantics, not a test suite.
Then the independent audit: check.rs re-validates the whole trace against the same invariants without the simulator’s success predicate, and verify_receipt re-executes run(experiment) and demands replayed == receipt.result, exact structural equality of every frame and counter. “Re-execution is the check; a hash alone is not evidence.” The honest caveat, kept in the docs: the checker is separate code in the same repository, “not independent external scientific replication”; the guarantee is recomputation by the same pinned artifact, which is exactly as strong as it sounds and no stronger.
persistence that replays
The durable layer extends the same determinism to storage. The expedition and habitat stores in crates/platonik-cli/src/journal.rs are append-only: entries named {revision:020}.json, each carrying previous_hash of its predecessor, committed by writing a temporary file, syncing it, and publishing through a hard link, a no-clobber atomic publication where an existing name is a conflict, never an overwrite.
Mutations are two-phase and idempotent. A habitat advance commits Started { until, from_hash, reserved_work } as its intent entry, then Completed { result } as a second revision; an interrupted pair leaves a pending intent that recover finishes, under its original --request-id, rather than abandoning or retrying blindly. Every mutating command takes --expect-revision (a compare-and-swap on history) and --request-id (retrying the identical request returns the original result; reusing the id for a different request fails). Export/import produces a checked platonik-habitat-bundle-v1 that is verified before it touches a separate restored save: the bundle is data to be validated, not state to be trusted. The subprocess tests even crash a writer at the two real publication boundaries (after temp-file sync, after the hard link) and confirm recovery preserves a valid old or new committed prefix, which the docs scope carefully as “process-interruption evidence on the tested filesystem,” not a universal durability proof.
what this buys, and what it doesn’t
The payoff is that evidence is cheap. Every recorded run is a self-contained object (experiment plus result plus two hashes) that any machine can re-derive; the aggregate CI gate replays the committed receipts on macOS and Linux, which is genuine cross-platform replay evidence. The saved-world layer makes “close the laptop, resume the expedition, get the identical trace” a verified property rather than a hope: the ark evaluation ran eight-advance save/reload histories and matched the uninterrupted receipt exactly.
The limits are stated plainly in the docs, and they are the point of the exercise. Determinism is established for the pinned artifact (the locked toolchain and the exact source), not for the concept on arbitrary hardware. The checker shares a repository with the simulator, so the independence is structural (different code, different entry point, same authors), not institutional. And the envelope stays small (16 cells, 128 ticks) because determinism is only as valuable as the cost of re-executing it; a world that takes a week to replay is a world whose receipts no one checks.
The transferable lesson is that deterministic simulation is a budget decision, not an infrastructure project. You need it precisely where a run is evidence, and you get it by making the real system deterministic: one seed, integer arithmetic, atomic commits, declared inputs, and a verification rule that runs the world again rather than trusting the record. FoundationDB bought that with a purpose-built simulator and a team. Platonik gets it with a clone-on-write activate and a policy that nothing enters the world unseeded. The scale differs by orders of magnitude; the discipline doesn’t.
sources
- platonik:
crates/platonik-core/src/sim.rsfor the tick pipeline andorder_ids;crates/platonik-core/src/policy.rsfor atomic activation;crates/platonik-core/src/check.rsforverify_receipt;crates/platonik-cli/src/journal.rsandhabitat_store.rsfor the append-only store - docs/rust-bridge.md: re-execution as verification, and the checker’s stated limits
- docs/engine.md: the tick and atomicity contract
- docs/continuous-habitat.md: save/advance/recover mechanics
- docs/agent-evaluation.md: interrupted-write recovery tests and their stated scope
- FoundationDB’s testing approach: the deterministic-simulation precedent this is scaled down from