db-16 step 02 — Deterministic simulator
Goal
Build a discrete-event simulator whose (seed, nodes, rounds) triple
completely determines its event log, and produce a canonical
serialization of that log.
Tasks
-
PRNG. Implement
splitmix64in each language with unsigned wrapping multiplication. Seed it per-decision withseed ^ (t << 32) ^ (s + 1)so that no shared mutable PRNG state crosses a(t, s)boundary. This eliminates the "whose turn is it to read the RNG?" ambiguity that bites every multi-language implementation. -
Per-tick decision. For each
(t < rounds, s ∈ 0..nodes), compute:dest_pre = (r & 0xFFFF) % (nodes - 1)then skip-self:dest = dest_pre + (1 if dest_pre >= s else 0).delay = 1 + ((r >> 16) & 0xFFFF) % 3.payload = (r >> 32) & 0xFF.
-
Scheduler. Maintain a min-heap of in-flight messages keyed on
(delivery_time, sender_id, global_seq).global_seqis a single monotonic counter incremented every time a message is enqueued. This guarantees a total order even when two messages have identical(delivery_time, sender_id). -
Tick loop. For
t in 0 .. rounds + MAX_DELAY:- Drain all heap entries with
delivery_time == t: for each, runrecvon the destination node, emit aRecvevent. - If
t < rounds: for eachs in 0..nodes, compute the decision, enqueue the message, runsendon the sender, emit aSendevent.
- Drain all heap entries with
-
Wire format. As documented in
CONCEPTS.md. Magic"DSE6",u32 LEevent count, thenevent_countevents. Each event uses little-endian integers and serializes its vector clock with entries sorted ascending by node id.
Acceptance
Inline unit tests:
splitmix64_known_values— forseed=0, the first three outputs are0xE220A8397B1DCDAF,0x6E789E6AA1B965F4,0x06C45D188009454F.sim_deterministic_one_node—--nodes 2 --rounds 3 --seed 1produces a fixed event count and a fixed first-event byte sequence.sim_event_count_formula— for any(nodes ≥ 2, rounds ≥ 1), total events =2 * nodes * rounds(every send becomes exactly one recv).causality_holds— after runningsimulate(...), walk the event log: everyRecvfrompeerhas a strictly-greater VC than the pairedSend.byte_round_trip— serializing the same event log twice yields identical bytes (no nondeterminism in the serializer itself).
All five green in Rust, Go, and C++.
Discussion prompts
- Why deliver before send within a single tick?
- What breaks if
global_seqis per-sender instead of global? - The simulator never drops or reorders messages beyond delay-based
interleaving. What new wire-format field would
--drop-rate pneed, and would it break the cross-language hash if defaulted to 0?