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>
17 KiB
Simulator BLOCKED — Stage 6 / §10.2 / §4.4 contracts unfulfilled
This file is filed per TESTING_SPEC §14. The local v1 parity bar is
green for the surface this implementation chose to bind to, but
three load-bearing contracts named by the plan and TESTING_SPEC have
not been satisfied at full scope. The remaining work is not a v1
"out-of-scope" item under TESTING_SPEC §15, so the only honest
artifact to ship is this BLOCKED.md plus the qualified BUILD_REPORT.md
next to it.
Iteration 14 fixed one of the four findings (Finding D — facade
surface descriptor now derived from trait declarations by
build.rs); the other three remain open.
Note on layout (added in path-refresh pass): since this file was
first filed, the workspace consolidated several crates
(runtime-facade, sim-detector, sim-driver, lint-deterministic)
into crates/simulation (PRs #39, #50, #52). Paths below reflect the
post-consolidation layout. The underlying contracts and the substantive
gaps they call out are unchanged.
| Symbol | Was | Now |
|---|---|---|
| Runtime facade traits | crates/runtime-facade/src/lib.rs |
crates/simulation/src/runtime/mod.rs |
| Surface-lock build | crates/runtime-facade/build.rs |
crates/simulation/build.rs |
| Surface fingerprint | crates/runtime-facade/surface.lock |
crates/simulation/surface.lock |
| Lint config | crates/lint-deterministic/banned.toml |
crates/simulation/banned.toml |
| Lint scanner | crates/lint-deterministic/src/lib.rs |
crates/simulation/src/lint.rs |
| Detector probes | crates/sim-detector/src/lib.rs |
crates/simulation/src/detector.rs |
| Sim binary entry | crates/sim-driver/src/main.rs |
crates/simulation/src/bin/sim-driver.rs |
A. plan §"Stage 6 — TIER CHECKPOINT" — distribution migration
Contract
Migrate
crates/distributionto the facade: every directInstant,SystemTime,tokio::spawn,tokio::time::sleep,rand::thread_rng,HashMapiteration,env::var,std::fs,std::netuse is rewritten against the runtime facade. Lint scope widens to include distribution; scan reports zero violations.
What's actually happening
The lint scanner's audit scope (crates/simulation/banned.toml:: [scope]::include) is exactly one directory:
include = [
"crates/simulation/src",
]
crates/distribution is not in scope. The crate still calls the
real runtime directly across at least these files:
crates/distribution/src/swim/member_list.rscrates/distribution/src/diagnostics/aggregator.rscrates/distribution/src/diagnostics/sink.rscrates/distribution/src/diagnostics/probes.rscrates/distribution/src/diagnostics/host_introspect.rscrates/distribution/src/diagnostics/collector/udp_echo.rscrates/distribution/src/diagnostics/collector/state.rscrates/distribution/src/diagnostics/collector/handlers.rscrates/distribution/src/cache.rscrates/distribution/src/iroh_driver.rscrates/distribution/src/peer_auth.rscrates/distribution/src/node_metadata.rscrates/distribution/src/kademlia/repair.rscrates/distribution/src/kademlia/directory.rs
These call sites use tokio::spawn, tokio::time::sleep,
std::time::Instant, std::time::SystemTime, rand::thread_rng,
std::net::UdpSocket, and std::collections::HashMap directly.
Live-fire reproduces the gap: drop a use std::time::SystemTime;
into crates/distribution/src/_violator_test.rs; the lint scan
still exits 0, because that directory is not in scope.
The problem, plainly
The whole point of the runtime facade is so that peer code becomes a
pure function of (spec, seed) — same inputs produce byte-identical
bundle outputs, in prod and in sim. Every direct Instant::now(),
tokio::spawn, or HashMap iteration in distribution breaks that
guarantee on the production diagnostics path: the diagnostics
aggregator's per-peer state lives in a HashMap, so its iteration
order varies run-to-run with hash randomisation. A replay can't
reproduce a recorded run if a HashMap::iter shows up between input
and output.
Until distribution is migrated, the "sim and prod are the same code with a different runtime" claim only holds for the simulation crate itself. The peer code that actually does the work is exempt.
Potential action items
The migration is big and has a forced order. A reasonable plan:
-
Grow the facade
Spawnsurface to be async-capable. The current trait (crates/simulation/src/runtime/mod.rs:91) only takesBox<dyn FnOnce() + Send + 'static>. Distribution's diagnostics pipeline is built ontokio::spawn(async move {...}). Addfn spawn_future(&self, fut: BoxFuture<'static, ()>)(or equivalent), implement onruntime::prodwithtokio::spawnand onruntime::simagainst the engine's fiber queue. This is asurface.lockbump. -
Decide on the
HashMappolicy and apply it crate-wide. Replace prod-pathHashMap/HashSetwithIndexMap/IndexSet(preserves insertion order, fast) orBTreeMap/BTreeSet(preserves sort order, slower but no extra dep). Aggregator, member-list, and kademlia routing-table state are the highest-leverage call sites — diagnostics aggregator first because it's the largest source of replay divergence. -
Migrate the call sites in waves. Suggested order, smallest blast radius first:
cache.rsandnode_metadata.rs(mostly time reads).peer_auth.rs(time + RNG).swim/member_list.rs(spawn + time + map iteration).kademlia/(spawn + time + map iteration).diagnostics/*(spawn + time + UDP + map iteration — biggest).iroh_driver.rs(depends on Finding C's facade growth — defer).
-
Widen lint scope. Add
"crates/distribution/src"tobanned.toml::[scope]::includeand fix the violations the scanner reports until it exits 0. Do this last, after the migration waves above land — otherwise the scanner blocks every intermediate commit. -
Add a live-fire regression test. After (4) is green, drop a throwaway
use std::time::SystemTime;somewhere indistribution/src/and confirm the scanner catches it; revert. Record the result in the iteration notes.
This is a multi-iteration project. Budget at least three.
B. TESTING_SPEC §10.2 — detector must run on the sim facade
Contract
The detector binary must run inside the reference scenario as an additional peer.
What's actually happening
crates/simulation/src/facade/sim/bundle.rs:81 calls
crate::detector::run_all() synchronously from inside the bundle
writer. The detector code lives at
crates/simulation/src/detector.rs and every D01–D12 probe body
reaches for the real std::* / tokio surface
(SystemTime::now(), Instant::now(), std::thread::spawn,
std::net::UdpSocket, std::net::TcpStream,
File::open("/dev/urandom"), gethostname, pid, allocator
addresses, ThreadId). Each violation is silenced with an inline
// lint-deterministic: allow … marker.
The detector is therefore observing the host runtime, not the sim
runtime. techniques.md admits this verbatim for D01 ("The v1 sim
does not intercept std::time::*; the detector running in-process
therefore reads the host wall clock") and D02 ("v1 reads the host
directly, so the two deltas are microseconds apart in practice").
t_detector::sim_indistinguishable consequently passes whether or
not the sim leaks — there is no sim facade between the probe and
the host to leak through.
The problem, plainly
NORTH_STAR's load-bearing claim is "no means, statistical or
otherwise, of detecting that it is in a sim." The test that's
supposed to enforce that claim is currently a no-op: the probes are
literally not pointed at the simulated runtime. It's the same as
running an integrity test against cat and concluding the data is
intact — there's no system under test.
The lint-deterministic: allow markers are load-bearing audit
evidence that this work was not done. Removing them without doing
the work would just move the failure from a semantic check to a
mechanical one.
Potential action items
Two changes have to land together; doing only one of them is worse than doing neither (it advertises a guarantee that isn't there).
-
Make the sim backend actually intercept the std/tokio surface.
crates/simulation/src/runtime/sim.rs(133 lines) is mostly stubs. Concretely:Clock::now()must return a virtual-time-derivedSystemTime, not pass through toSystemTime::now().Udp::bindmust return a socket whosesend_to/recv_fromride the engine's link graph instead of OS sockets.Spawnmust enqueue onto the engine's fiber scheduler instead oftokio::spawn.ProcessMeta::hostname/pidmust return per-node sim values, not the host's.
-
Route the detector probes through the facade. Open
crates/simulation/src/detector.rsand, for each probe:- Replace
SystemTime::now()withfacade.clock().now(). - Replace
Instant::now()withfacade.clock().monotonic(). - Replace
std::thread::spawnwithfacade.spawn().spawn(...). - Replace
std::net::UdpSocket::bindwithfacade.udp().bind(...). - Drop the
// lint-deterministic: allow …markers as each site converts. By the end the file should be allow-marker-free.
- Replace
-
Run the detector as a real peer. Add a
hostof kinddetectorto the reference scenario spec. In the bundle writer (crates/simulation/src/facade/sim/bundle.rs:79–88), delete the in-processdetector::run_all()call. Instead, have the engine schedule a fiber on that host that runs the same probes via the sim facade and writes its verdict stream into the bundle just like any other host's records. -
Tighten the test. Once the above is in, modify the sim facade in a controlled way to leak (e.g., have
Clock::now()return wall-clock instead of virtual). The relevant probe verdict should flip fromIndistinguishabletoDetectedSimandt_detector::sim_indistinguishableshould fail.
This depends on Finding A only for the Spawn async surface
growth, so it can begin in parallel with A's migration waves.
C. plan §"Stage 6 — TIER CHECKPOINT" — real transports on the sim facade
Contract
Real
quinnlinked against sim UDP — no QUIC fork. Real iroh stack (MagicSock, discovery, NodeMap, connection cache) linked against sim facade — no iroh fork.
And TESTING_SPEC §5.2:
The same
quinn/iroh/iroh-relay/distribution/swactorsource files run in both binaries.
What's actually happening
crates/simulation/src/engine.rs (885 lines) is a discrete-event
record synthesiser. Its imports are std::cmp::Ordering,
std::collections::{BTreeMap, BTreeSet, BinaryHeap},
serde::{Deserialize, Serialize}, and types from crate::spec.
grep -n 'quinn\|iroh\|distribution\|swactor' crates/simulation/src/engine.rs returns zero matches.
The engine emits HostStart / HostStop / HostCrash /
Snapshot / Mutation events on a synthetic loopback host and
writes them to the bundle. The corpus is shaped to look like real
peer-code output, but no production peer code is in the call
graph.
(Update from the original BLOCKED.md: the sim-driver binary used
to pin #[used] static references to defeat dead-code stripping —
the t_same_binary::symbol_overlap test exists to detect that
gimmick. The pin gimmick was removed during the consolidation;
symbol_overlap is now listed as removed in
tests/parity-bar/expected_failures.txt:33. The deeper claim is
unchanged: peer code is not exercised through the sim engine.)
The problem, plainly
§5.2's whole point is that there should be one implementation of QUIC, one implementation of iroh, one implementation of distribution — and the only difference between prod and sim is which backend implements the runtime facade. If the sim binary runs synthesised events that mimic peer-code output, then the sim is not testing peer code — it's testing the simulator's model of peer code, which is exactly the thing the parity bar is supposed to make unnecessary.
This is why the dependency chain matters: real quinn needs an
async UDP socket, real iroh needs quinn plus a tokio runtime
and DNS, real distribution needs iroh::Endpoint. Each layer
sits on the one below it.
Potential action items
This is the largest body of work in the BLOCKED set. A reasonable sequencing:
-
Async UDP on the facade. Grow
traits::Udp/traits::UdpSocket(crates/simulation/src/runtime/mod.rs:64– 73) from the current syncbind / send_to / recv_fromshape to something that implementsquinn::AsyncUdpSocket. In prod this wrapstokio::net::UdpSocket; in sim it wraps a queue tied to the engine's link graph and clock.surface.lockbump. -
Virtual
tokio::time::Instantprojection. iroh's internals calltokio::time::Instant::now()andtokio::time::sleepdirectly (it does not consult our facade). Either (a) build a sim-tokio runtime sotokio::timereads from the engine's virtual clock when iroh is linked into the sim binary, or (b) accept that iroh-as-shipped can't run under the sim and patch iroh upstream. (a) is the path TESTING_SPEC §5.2 implies. -
Async
Spawn. Same growth as Finding A (1); A and C share this prerequisite. -
DNS table in sim.
traits::Dns(crates/simulation/src/runtime/mod.rs:85) needs to answer relay-URL queries from a per-node DNS table the spec configures, not from the host resolver. iroh's relay discovery won't work otherwise. -
Build a
quinn::Endpointfrom facade pieces. Once (1) and (2) are in, construct aquinn::Endpointfrom the facade's UDP socket and feed it to iroh'sMagicSocksetup. -
Re-point
distribution. Wiredistribution's transport construction off directiroh::Endpoint::builder()calls and onto the facade-built endpoint. (Finding A's migration is a precondition.) -
Replace synthetic engine emission with real peer-code execution. This is the big one.
engine.rscurrently is the event source; under (6), real peer code becomes the event source and the engine becomes scheduling + link graph + clock. Most ofengine.rs's synthetic emit logic deletes; the schema-floor parity bars then bind to events real code produced. -
Re-enable the parity-bar checks currently in
expected_failures.txt.t_replay::*, the §6/§7 schema coverage suite, andt_same_binary::shared_load_bearingare parked behind Stage 6/7. They light up as the real-peer-code path comes online.
Expect this to span four to six iterations. Don't try to land it as one PR.
D. TESTING_SPEC §4.4 — facade surface lock — fixed in iteration 14
Contract
Each trait's method signatures hashed in declaration order.
Resolution
Was: SURFACE_DESCRIPTOR = include_str!("surface_descriptor.txt"),
a 38-line hand-edited file. A trait edit that did not also touch
the text file passed the lock unchanged.
Now: crates/simulation/build.rs parses
crates/simulation/src/runtime/mod.rs with syn at build time,
walks pub mod traits, emits one line per trait method signature
into $OUT_DIR/surface_descriptor.txt, and the runtime module
embeds the build output via include_str!. Live-fire verified:
adding fn stealth_method(&self) -> u32 to pub trait Clock flips
the descriptor hash and fails live_fingerprint_matches_locked.
Reverted.
This finding is closed. Listed here for the record because the other three are open against the same TIER CHECKPOINT.
What the next agent should do
Either:
-
Pick up the chain at A → B → C in order. A is the prerequisite for the lint widening; A's facade growth (async
Spawn) and B's sim-facade interception together unlock C's real-transport linkage. Plan on multiple tier-checkpoint-sized iterations; this is not a single-iteration cleanup. -
Raise a TESTING_SPEC defect under §14.2 if a specific check is itself wrong — e.g., if §10.2's "additional peer" wording proves impractical given the workspace constraint
no-modify src/and the facade should instead intercept at the bundle-writer boundary. A defect would need to quote NORTH_STAR / SPEC / OBSERVABILITY and ship as a separate commit titledTESTING_SPEC: correct §N.Mper §14.
What the next agent should not do is widen the
lint-deterministic: allow markers, leave the engine wholly
synthetic, or paper over symbol-presence checks. The bar in this
repo is what the spec says.