swactor/crates/simulation/SPEC.md
Zachery Aaron Shores-Chmielewski 1f8a67231c feat(sim): rebuild around discrete-event engine
Replaces the generic SimNode/gossip/dashboard framework with a virtual-time
discrete-event engine (priority queue ordered by time/node/fiber/seq), a TOML spec
parser, bundle writer, replay, divergence detector, lint, and postproc, plus the
parity-bar test harness with fixtures and xtask parity-lock tooling. Rewrites
transport identity/crypto and adds the SPEC/TESTING_SPEC/OBSERVABILITY/NORTH_STAR docs.


Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-05-21 23:48:02 +04:00

716 lines
30 KiB
Markdown

# Simulator — implementation specification
Working spec for the simulator described in [`NORTH_STAR.md`](./NORTH_STAR.md).
Read that first. The observable surface this spec commits the sim to
reproduce lives in [`OBSERVABILITY.md`](./OBSERVABILITY.md); every
obligation below cites the subsection it discharges.
## Table of contents
1. Scope and non-goals
2. Engine
3. Runtime facade
4. Network model
5. Hosted entities
6. Recording format
7. Replay
8. Observability
9. Calibration loop
10. Glossary
---
## 1. Scope and non-goals
**In scope.**
- The network the node-under-test touches: arbitrary topology, per-
link physics (bandwidth, delay, loss, MTU), NAT and middlebox
behavior, reachability, partitions, healing.
- The transports that run over it (iroh today; others as added).
Transports are first-class: their internal state — connection
cache, NAT bindings, congestion control — must be observable on
the same schema as production. See OBSERVABILITY §3.6, §4.3, §4.6.
- The hosted peers: any node our code can run is sim-native; any
node we cannot rebuild (third-party binary, foreign version) is
accommodated via the opaque-binary escape hatch (§5.2).
- The shared infrastructure those peers depend on: relays,
signaling, DNS, vast.ai-style host metadata (§4.4, §4.7,
OBSERVABILITY §3.10).
- A deterministic engine (§2) so calibration has a stable baseline.
- A recording format identical to the production recording format
(§6, OBSERVABILITY §3).
**Out of scope.**
- The algorithms under test. SWIM and whatever swactor hosts run as
the same code paths in sim and prod. The sim does not stub them.
- The CPU / memory cost of running each peer's logic. We assume
peers fit; the host-budget model (§4.8) only accounts for what is
visible to the peer from the host's perspective.
- Anything from OBSERVABILITY §5 ("Out of scope"). The sim is not
obligated to model kernel system calls, GPU compute internals,
allocator behavior, or CPU performance counters. If that list
shrinks, this scope grows accordingly.
- One-time configuration tooling. Topology specs are inputs; how
they are authored is outside this spec.
**Non-goal: faster than wall-clock.** Speed is a nice property of
discrete-event simulation, not a requirement. The bar is *fidelity*,
measured against the calibration loop (§9). A run that completes in
half the time but diverges on a tier-2 observation is worse than a
run that takes twice as long but matches every distribution.
## 2. Engine
### 2.1 Determinism contract
Same input, same output, byte for byte. "Input" means: the topology
spec, the link policies, the peer set, the workload, and the seed.
"Output" means: every record emitted to the recording (§6), in the
same order, with the same values. Determinism extends to:
- Per-node `monotonic_seq` counters (OBSERVABILITY §3.3).
- `snapshot_id` strings (§3.4 there).
- Trace IDs once §4.2 lands (the deterministic RNG is the source).
- Any timing field driven by virtual time (§2.2).
Non-determinism that would cross this boundary — `SystemTime::now()`,
`tokio::spawn` ordering, `HashMap` iteration order seeded from the
process — is removed at the runtime-facade layer (§3) before it can
leak into peer code. The list of sources we close off lives in §2.4.
The contract is enforced by a divergence detector (§2.5). It is
*also* a calibration assumption: when a sim run and a prod run
diverge in distribution, we need to know that the sim run, at
least, is reproducible from its seed. If the sim is also
non-deterministic, calibration loses its baseline.
### 2.2 Virtual time and the event loop
The engine drives a discrete-event loop over a virtual-time
priority queue. Every action that takes time — a packet in flight,
a timer firing, a sleep — yields to the engine, which advances
virtual time to the next scheduled event and resumes the relevant
fiber.
Virtual time is the basis of every `wall_ms` field in the recording
(OBSERVABILITY §3.1, §3.3, §3.4). Sim-run `wall_ms` values are
exactly virtual-time millis; the §3.5 clock-alignment math sees
zero drift, which is the correct answer (and the parity check
should see zero drift on sim runs and bounded drift on prod runs).
Peer code does not observe virtual time directly. It calls the
runtime facade (§3), whose sim implementation hooks into the event
loop. From peer code's perspective, it ran in time — the time was
just controlled by the engine rather than the kernel.
### 2.3 Scheduler and executor
The engine owns a single-threaded executor that drives every fiber
in the run. Fibers correspond to peers, transport tasks, SWIM
ticks, snapshot timers, etc. The executor's pick order is
deterministic (sorted by virtual-time priority, then by fiber-id
tiebreaker).
When two events are scheduled at the same virtual tick, the
tiebreaker is **(node_id, fiber_id, event_seq)**. Same input ⇒
same tiebreaker resolution. No part of the executor ever falls
back to wall-clock ordering or thread-local randomness.
The OBSERVABILITY §4.5 actor-mailbox surface flows out of the
executor: per-actor mailbox depth and per-task age are the
executor's own state, exposed to the recording on every snapshot.
### 2.4 Sources of non-determinism
Each source below has a defined sim-side handling. Anything not on
this list that leaks non-determinism is a bug.
| Source | Sim handling |
|---|---|
| `SystemTime::now` | Facade-routed to virtual time. |
| `std::time::Instant` | Facade-routed to virtual time. |
| `tokio::spawn` ordering | Executor (§2.3) imposes deterministic order. |
| `HashMap` / `HashSet` iteration | Replaced with deterministic-iteration containers behind the facade. |
| Channel select races | Engine resolves via fiber-id tiebreaker. |
| OS UDP / TCP socket APIs | Routed to the simulated network (§4). |
| DNS resolution | Routed to the simulated DNS (§4.4). |
| `getrandom` / `rand::thread_rng` | Per-node deterministic RNG seeded from `(run_seed, node_id, stream_label)`. |
| File-system access | Per-node sandbox rooted at `{run_dir}/{node_id}/`; reads of system files (`/proc/net/udp`, `/etc/resolv.conf`) routed to the simulated host model (§4). |
| Process spawn | Sim-native: a new fiber, not a new OS process. Opaque-binary case is §5.2. |
| `std::env::var` | Routed to per-node env table from the topology spec. |
| Iterator orderings that depend on `Box<dyn Trait>` vtable addresses | Avoided at the facade boundary; any helper that returns trait objects has a defined-order wrapper. |
### 2.5 Replay and divergence detection
Running the engine with the same `(spec, seed)` twice produces
identical records (§2.1). Running it with the same `(spec, seed)`
and a *different* peer-code version produces records that diverge
at the first observable difference; the engine detects this by
hashing each emitted record's bytes and comparing against the
stored hash chain (if a baseline run is supplied) or storing one
(if a baseline is not). Divergence is a structured `Error` event
plus a non-zero exit; the bundle is preserved for inspection.
Divergence detection is the engine-level mechanism. The
*calibration*-level mechanism (sim vs. prod) is §9; the two are
distinct because sim-vs-prod is statistical, while sim-vs-sim is
exact.
## 3. Runtime facade
### 3.1 Surface
The facade is the narrow trait family that peer code calls instead
of the raw `std::*` / `tokio::*` / OS APIs. Its surface is
*exactly* what the peer code needs and nothing more. Concretely:
- A clock (`now`, `sleep_until`, `interval`).
- An UDP socket (`bind`, `send_to`, `recv_from`).
- A QUIC endpoint (iroh's `Endpoint` shape; not the OS UDP).
- A DNS resolver (`resolve_a`, `resolve_aaaa`).
- A spawn primitive (`spawn`, `spawn_local`).
- A randomness source (`get_rng()` returning a streamed RNG).
- A file-system handle (`open_read`, `open_write`) scoped to the
per-node sandbox.
- An env-var read.
- A process metadata read (`hostname`, `pid`).
Anything the recording observes (OBSERVABILITY §3) is observable on
the facade — either because the facade *emits* the corresponding
event itself (e.g., `MessageSent`), or because the subsystem
calling the facade emits it.
### 3.2 Prod implementation
Each method delegates to the obvious real backend: tokio runtime,
std net stack, libc DNS, etc. The implementation lives in the
existing crates (`crates/distribution` and friends). Production
code paths never see a sim-aware branch — the swap is at the
facade-trait boundary, not at every call site.
### 3.3 Sim implementation
Each method delegates to the engine. `now` reads virtual time;
`sleep_until` schedules a wake-up event; UDP send hands a packet
to the network model; DNS reads the simulated zone; `spawn`
registers a fiber with the executor; `get_rng` returns a stream
keyed by `(node_id, stream_label)`.
The sim implementation owns the runtime facade for *all* sim-native
hosts in the run, in one OS process. Opaque-binary hosts (§5.2)
get their own runtime facade implementation that virtualizes the
boundary at the syscall level instead.
### 3.4 Build-time switching
The active implementation is chosen by a single Cargo feature. No
runtime branching, no `if cfg!(sim)` sprinkled through peer code.
Two binaries are built from the same source: a prod binary linked
against the prod facade, and a sim driver that links the sim
facade and exposes the topology-spec entry point. Peer code does
not need a `Cargo.toml` change to be used in the sim — being
written against the facade trait is sufficient.
## 4. Network model
The network is what peer code talks through. Its job is to be
*indistinguishable* from a real network on every channel the
recording observes (OBSERVABILITY §3.2, §3.3, §3.6, §3.7, §4.1).
### 4.1 Topology
A topology is a directed graph of nodes and links plus a set of
shared-infrastructure entities (relays, DNS zones, vast.ai-style
host metadata sources). Topologies are declarative — they are an
input to the engine, not assembled imperatively at run time.
Graphs are arbitrary. The sim does not enumerate named topologies;
it accepts whatever the spec describes. The spec lists each node's
role, its peer set, and the link to each peer (or to the shared
infrastructure). Asymmetric links are first-class:
`bandwidth(A→B) ≠ bandwidth(B→A)`, and `loss(A→B) ≠ loss(B→A)`,
without special-casing.
Partition and heal are topology mutations applied at scheduled
virtual times. Each mutation is a record on the recording stream
(emitted as a sim-only `Custom` event, OBSERVABILITY §6 rule 4) so
the post-processor can correlate it with peer-observed events.
### 4.2 Link physics
Each directed link carries:
- `bandwidth_bps` — capacity. Excess packets queue (per the queue
policy) or drop (per the drop policy).
- `one_way_delay_ms` — base latency. Distributions (lognormal, etc.)
are allowed; the spec declares which.
- `jitter_ms` — per-packet delta on top of the base.
- `loss_rate` — independent or correlated; the spec declares which.
- `mtu` — packets above MTU are fragmented per the L3 / L4 rules.
- `queue_policy` — FIFO / SFQ / FQ-CoDel; spec-declared.
These are the *applied* values; OBSERVABILITY §4.8 commits the sim
to also emit them on every snapshot in the `links_applied` block
(sim-only field) so a replay knows what conditions it ran under.
The *observed* values flow out of the same passive-estimation code
prod uses, on the `links` block (sim and prod).
Per-link parameters can vary over time per the spec. Time-varying
parameters are also part of the topology declaration; they are
applied at scheduled virtual times, the same way partitions are.
### 4.3 L3 — IP
The sim addresses every host with one or more IPv4 and IPv6
addresses. The address assignment is in the topology spec.
Per-host interface metadata (name, MTU, up/down) is read by the
host scrape (OBSERVABILITY §3.8); the sim populates it from the
spec.
IP-level behavior modelled:
- Per-packet TTL decrement at every hop. Packets with TTL=0 are
dropped and the drop is emitted as a `WirePacket` (§4.1 in
OBSERVABILITY) with outcome `dropped_ttl`.
- ECN bits passed through (no AQM model unless the spec adds one).
- Fragmentation when MTU is exceeded. Reassembly at the destination,
with the standard timeout. Reassembly failures observable.
- Source-address validation. Unspoofable per default; the spec can
override per-link to allow spoofing for adversarial scenarios.
IP-level behavior *not* modelled: ICMP error generation beyond the
two cases needed for path-MTU discovery (`Fragmentation Needed`)
and dead-peer detection (`Port Unreachable`). The sim emits both
when the corresponding condition occurs.
### 4.4 NAT and middleboxes
NATs are first-class entities in the topology, with type drawn from
{Full-Cone, Restricted-Cone, Port-Restricted-Cone, Symmetric}. Each
NAT maintains a binding table; bindings expire per the spec's
keepalive-timeout setting and are refreshed by outbound packets.
OBSERVABILITY §4.6 commits the sim to expose the binding table on
every snapshot (`nat_bindings` per peer). The sim's authoritative
table is the source; the recording shows the binding from the
hosted side. NAT-binding refresh interval and refresh count match
real-world dynamics under each modelled NAT class.
Beyond NAT, the spec can place middleboxes that drop packets by
DPI signature (used for adversarial tests of relay traffic).
Middlebox drops are observable as `WirePacket` outcomes.
### 4.5 L4 — UDP, TCP, QUIC
**UDP** is a thin wrapper over the L3 model: send-to / recv-from,
no flow control, drops surfaced as silent loss. Probes
(OBSERVABILITY §3.9) ride on this layer.
**TCP** is reserved for shared-infrastructure protocols (e.g., the
collector HTTP sink). It is modelled as a faithful loss-recovery
implementation, but its connection state is *not* exposed in
tier-2 because production iroh does not use it for peer traffic.
**QUIC** is the load-bearing L4. The sim runs the same QUIC
implementation iroh runs in prod (quinn), linked against the sim
UDP. The QUIC handshake, congestion control, and stream multiplexing
behave exactly as they do in prod because the *implementation* is
the same; only the underlying UDP is the sim's. This is what makes
the OBSERVABILITY §4.3 (congestion-control state) and §4.7
(handshake records) parity bars achievable: the data structures
reporting on each are the real ones.
### 4.6 Endpoint stack
Each sim host runs the full iroh stack (or its equivalent for
non-iroh transports): MagicSock, discovery, NodeMap, connection
cache. These are *not* re-implemented; they are the production
code linked against the sim runtime facade. That is the only way
their internal state — the state surfaced in OBSERVABILITY §3.6
(`Tier2IrohState`) — matches prod by construction.
Per-node configuration (relay set, discovery providers) is from
the topology spec. The DNS resolver used by iroh's relay-URL
lookup is the simulated one (§4.4 in this spec — *not* the OS
resolver — the simulated host model).
### 4.7 Clocks
Each host's clock is virtual time plus a per-host skew and drift
configured by the spec. Skew is a constant offset; drift is a
rate (ppm). Clock samples (OBSERVABILITY §3.5) capture the
relationship between each host's clock and the collector's
("virtual UTC"), so the post-processor's alignment code does the
same work on sim and prod bundles.
A host can experience a clock jump (e.g., NTP step) at a scheduled
virtual time. The jump is a topology mutation; it is observable
because every event after it carries the new `wall_ms` while
`monotonic_seq` (OBSERVABILITY §3.3) keeps going up.
### 4.8 Host budget
Each host has a modelled budget: CPU shares, memory bytes,
open-fd count. These map to the OBSERVABILITY §3.11 process-stats
fields. The sim does not enforce the budget against peer code
(peer code runs as fibers); instead, the budget is tracked
indirectly:
- CPU time per fiber is approximated from the number of times the
fiber yields per virtual-time unit, weighted by the per-host CPU
share. The result feeds the `cpu_ms` field on the process snapshot.
- Memory is tracked from the peer code's own allocations against
the per-node allocator wrapper. RSS is reported as the
high-water mark seen since the previous snapshot. VmSize is the
current live byte count.
- Open-fd count is the number of sim-side sockets currently bound
by the node (UDP, QUIC, TCP, plus simulated files).
This is the imperfect-but-honest implementation NORTH_STAR §"What
it models" calls for: the recording surface is populated; the
underlying numbers are approximations, marked as such in the
post-processor's documentation; calibration against prod will tell
us when the approximations are too loose.
## 5. Hosted entities
### 5.1 Sim-native hosts
A sim-native host is a peer whose code we own and rebuild against
the sim runtime facade. It is the same binary in spirit as the
prod binary — same crates, same code paths — linked against a
different runtime crate.
A run hosts arbitrarily many sim-native hosts. Each has its own
address space, its own runtime facade instance, its own
diagnostics aggregator (OBSERVABILITY §3). The engine routes I/O
between them through the network model.
Sim-native hosts cover all of OBSERVABILITY §3's parity bar
because they are running the production diagnostics stack — every
event and snapshot is emitted by the same code that emits them in
prod.
### 5.2 Opaque-binary hosts via virtualized I/O
When we need to host a peer whose code we cannot rebuild against
the facade — a third-party node, a peer running an old version,
a kernel that handles a packet a specific way — the sim drives
the real binary through a virtualized I/O boundary. The binary
runs as a real OS process; its socket calls go through a shim
(LD_PRELOAD-style on Linux, or a network-namespace + tun device
on platforms where syscall interception is brittle); the shim
routes packets to the engine the same way sim-native UDP does.
From the binary's perspective, it sees a real kernel, real sockets,
real time. The engine controls what crosses the wire. Records
emitted by the opaque binary (if any) are read off its own log
output and translated into the recording schema as best as the
adapter can manage; gaps appear as `None` fields, marked.
Opaque-binary hosts are an escape hatch, not the default. The
parity bar applies fully to sim-native hosts; opaque-binary hosts
have inherently incomplete recording (we cannot fully instrument
a binary we did not build).
### 5.3 Lifecycle
Every host has a defined lifecycle in the topology spec:
- `start_at_ms` — virtual time at which the host's runtime is
brought up. Boot order is deterministic.
- `restart_at_ms[]` — scheduled restarts. `boot_sequence`
(OBSERVABILITY §3.1) increments per restart.
- `stop_at_ms` — clean shutdown.
- `crash_at_ms` — uncontrolled exit. Distinct from `stop` because
no draining occurs.
Each lifecycle event is a topology mutation; each is observable as
a `Custom` event on the recording (the host that experienced the
event reports it through its own diagnostics, if reachable; the
engine reports it as a sim-only record otherwise).
## 6. Recording format
### 6.1 Wire-level trace
The wire-level trace is the per-packet stream described in
OBSERVABILITY §4.1. The sim emits one record per packet in either
direction at every modelled hop. Records are written to
`{run_dir}/wire/{node_id}.ndjson` and tarred into the bundle.
Wire-level capture is gated by a per-run setting. Off by default
because the data volume is high; on for calibration runs and for
any run where the §4.1 OBSERVABILITY parity bar is being checked.
When a payload-bytes capture is requested, the bytes live next to
the trace as a separate file (`wire-payloads/...`) so the trace
itself stays small enough to grep through.
### 6.2 Internal-observation trace
This is the production recording surface itself — OBSERVABILITY §3
in its entirety. The sim writes the same files to the same paths
the production collector does, under
`{collector_root}/{run_id}/{node_id}/`:
```
boot.json
snapshots/{snapshot_id}.json
events/{batch_seq}.json
finalize.json
```
A sim run produces a bundle byte-for-byte indistinguishable from a
prod bundle of the same workload (modulo the sim-only fields
flagged in OBSERVABILITY §3.11 / §6.5 below). That isomorphism is
the parity bar.
### 6.3 Schema versioning
The schema is versioned. Every record carries a `schema_version`
field at the top of its envelope. The post-processor accepts any
version it knows about. Schema upgrades are documented in the
diagnostics crate's CHANGELOG; the sim and prod sides upgrade
together (NORTH_STAR §"Calibration is ongoing").
Adding a field to a record is a minor version bump; removing or
renaming is major. Old bundles remain readable forever; the
post-processor handles back-compat. The sim's emitted version
matches whatever version of the diagnostics crate it is linked
against, the same as prod.
### 6.4 Storage layout
Bundles are tarballs (`.tar.gz`) at
`{collector_root}/bundles/{run_id}.tar.gz`. The internal layout
matches `DIAGNOSTICS_PLAN.md` §"Bundle Format":
```
{run_id}/
MANIFEST.json
orchestrator/{boot, snapshots/, events/, finalize}.json
stage-N/{boot, snapshots/, events/, finalize}.json
collector.log
summary.md # post-processor output, optional
```
Sim runs additionally write:
```
{run_id}/
sim/
spec.toml # the topology spec used
seed # the run seed
wire/{node_id}.ndjson (if wire-level capture is on)
links_applied.json # ground-truth link params over time
mutations.log # partition/heal/restart events
```
The `sim/` subtree is sim-only and is ignored by the parity diff.
It is what enables a sim-vs-sim replay (§2.5) and what gives a
calibration run the ground truth to compare against.
### 6.5 Sim-only fields
Sim runs emit a small number of fields prod cannot: ground-truth
applied link parameters (`links_applied`, OBSERVABILITY §4.8),
sim-internal scheduler decisions (OBSERVABILITY §6 rule 4),
RNG-state traces. These are written to `sim/` (§6.4) and are never
read by code that also runs in prod. The node-under-test cannot
observe any of them.
## 7. Replay
### 7.1 Environment restoration
Replay takes a recording (produced by a prod run, a sim run, or
either one with a partial bundle) and plays back the *environment*
the recorded run was in. Concretely:
- Topology is reconstructed from the recording. NORTH_STAR §"Two
modes, one engine" demands this be feasible from a real-world
bundle, so the per-link parameters used by the sim must be
derivable from what the recording exposes. Where the recording
exposes only `links` (observed), the sim seeds its `links_applied`
with the observed values and lets the parity diff quantify any
drift. Where the recording exposes `links_applied` (sim-origin),
the sim adopts them exactly.
- Peer set is reconstructed from the boot identity blocks across
the bundle.
- Mutation schedule (partition, heal, restart) is reconstructed
from the `Custom` events the original run emitted.
- Shared infrastructure (relays, DNS, vast.ai metadata) is
reconstructed from whatever the bundle's host-scrape and
identity blocks recorded.
### 7.2 Peer re-execution
Per NORTH_STAR §"Two modes, one engine": peers in a replay are
**re-executed peer code**, not stubs driven by recorded outputs.
The recording carries the *environment*; the peer logic that
runs against that environment is whatever version is under test
in the replay. That is how a replay tests a candidate change — by
exposing the same world to new code.
When the recording lacks information needed to drive a peer
faithfully (a third-party peer whose code we do not have), that
peer is hosted via the opaque-binary path (§5.2). If the original
recording was prod and the peer was iroh-stack swactor, the
sim-native path applies.
### 7.3 Divergence handling
A replay diverges when a re-executed peer makes a different choice
from what the recording shows the original peer made. Divergence
is *expected*: that is the point of replay — to see whether the new
code behaves differently against the same conditions.
The replay records the new behavior at the same level of detail
(§6) and the post-processor's diff mode is what surfaces the
delta. Divergence is not an error; only when the divergence is
*outside the parity envelope* (§9.3) is it a sim bug rather than
an algorithm-change observation.
## 8. Observability
### 8.1 Shared sinks with prod
The sim's diagnostics aggregator (the one running inside each
sim-native host) is the *production* diagnostics aggregator from
`crates/distribution/src/diagnostics`. It is linked against an
HTTP sink whose target is the sim's collector — itself a real
binary (also from `crates/distribution`), brought up by the engine
on a virtual host at the start of every sim run.
The collector writes the same bundle layout (§6.4). The
post-processor reads it the same way. The "shared" in "shared
sinks" means literal: the same code runs on both sides of the
sim/prod boundary, so the parity bar is enforced by construction
for every record kind enumerated in OBSERVABILITY §3.
### 8.2 Sim-only diagnostics
The sim emits a small set of additional records the node-under-test
cannot observe (OBSERVABILITY §6 rule 4): scheduler decision logs,
exact virtual-time tick streams, RNG-state traces, per-link
applied parameters (§6.5). They are written to the `sim/` subtree
of the bundle and are read only by the engine's own debugging
tooling, never by code that also runs in prod.
The discipline: any time we are tempted to add a sim-only field
that the node-under-test *can* observe, we have to either
(a) commit to making prod observe it too and put it in
OBSERVABILITY §3 or §4, or (b) keep it strictly under §8.2. There
is no third option.
## 9. Calibration loop
### 9.1 Procedure
Calibration is the act of measuring sim-vs-prod observable
equivalence. The loop is concrete:
1. Run a workload in prod. Collect the bundle.
2. Extract the topology + mutation schedule from the bundle (§7.1).
3. Run the *same* peer-code version in the sim against that
reconstructed environment. Collect the bundle.
4. Diff the two bundles, record kind by record kind, against the
parity bars defined in OBSERVABILITY §3 / §4.
5. Anywhere the distributions diverge beyond the noise floor
(§9.2), file a sim bug. Anywhere prod is missing a record the
sim emits or vice versa, file a recording bug or a sim bug
(whichever direction the asymmetry runs — see OBSERVABILITY
§6 rule 3).
The loop runs continuously, not as a one-time validation. Every
real deployment is evidence; every divergence is an issue.
### 9.2 Noise-floor estimation
Prod runs are inherently noisy: real link latencies jitter, real
kernels make scheduling decisions we cannot reproduce, real clocks
drift. Calibration cannot demand sim records to be byte-identical
with prod records — it demands them to be *statistically
indistinguishable* (NORTH_STAR §"The parity bar").
The noise floor is estimated from prod-vs-prod variance: two prod
runs of the same workload, under as-matched-as-possible
conditions, diff each other. The variance of that diff defines
the noise floor for each numeric field; a sim-vs-prod divergence
within that envelope is not a bug. A sim-vs-prod divergence beyond
it is.
Establishing the noise floor is an ongoing project — every new
metric needs its own estimate. The post-processor's calibration
report ships per-metric noise-floor numbers alongside the
sim-vs-prod diff.
### 9.3 Parity metrics
Per OBSERVABILITY's call for distributional parity, the
calibration report tracks for each metric:
- **Presence parity** — does the sim emit a record whenever prod
does (and vice versa)? Boolean per record kind.
- **Mean parity** — is `mean_sim ≈ mean_prod` within the noise
floor of the means?
- **Tail parity** — is `p99_sim ≈ p99_prod` within the noise floor
of the tail? This is the bar OBSERVABILITY calls out
explicitly; mean parity alone is not enough.
- **Causal parity** — does the sim emit events in the same order
prod does, conditioned on the same upstream causes? Tied to the
causal-trace-IDs surface (OBSERVABILITY §4.2).
A pass is "every metric within noise floor on every parity
dimension." A fail names the metric and dimension. The
post-processor's calibration report is structured exactly that
way; "did calibration pass" reduces to "did any line in the
report come back red."
## 10. Glossary
- **Aggregator** — per-process recording state owner. Owns the
reachability log, event ring, snapshot assembler, sink. Same
type in sim and prod.
- **Bundle** — the tarball at end-of-run containing every record
emitted during the run. See §6.4.
- **Calibration** — measurement of sim-vs-prod observable
equivalence. See §9.
- **Collector** — the HTTP server every aggregator POSTs to.
Centralized per-run target; writes the bundle.
- **Engine** — the discrete-event simulator core. Owns virtual
time, the executor, the network model, the topology graph. See
§2.
- **Facade** — the trait surface peer code uses for all
potentially-non-deterministic operations. Two implementations
(prod, sim). See §3.
- **Fiber** — an executor-managed unit of concurrency. One fiber
per peer task; per-fiber scheduling is deterministic. See §2.3.
- **Mutation** — a scheduled change to the topology during a run
(partition, heal, restart, link-parameter change). Recorded as
a `Custom` event. See §4.1.
- **Noise floor** — the variance between two prod-vs-prod runs of
the same workload. Sets the bar that sim-vs-prod divergence is
measured against. See §9.2.
- **Opaque binary** — a hosted entity whose code we do not own and
cannot rebuild against the facade. Driven via syscall
interception. See §5.2.
- **Parity bar** — the requirement, per OBSERVABILITY, that every
record kind on the observable surface match between sim and
prod. See OBSERVABILITY §6.
- **Recording** — the on-disk artifact of a run's observable
surface. Same format in sim and prod. See §6.
- **Replay** — re-execution of peer code against an environment
reconstructed from a recording. See §7.
- **Run** — one execution of the engine from boot to finalize.
Identified by `run_id` (in the bundle) and `run_seed` (input).
- **Sim-native host** — a peer whose code is linked against the
sim runtime facade. See §5.1.
- **Topology spec** — the input declaration of nodes, links,
shared infrastructure, mutations, and run parameters. See §4.1.
- **Virtual time** — the engine's authoritative clock.
Wall-clock `wall_ms` fields in the recording are populated from
it on sim runs. See §2.2.