db-19 step 03 — Cross-language determinism

Goal

Lock the byte-level output of all three implementations (Rust, Go, C++) to the same sha256 for every canonical scenario in scripts/cross_test.sh. This is the difference between "ZAB works in my language" and "ZAB is this exact state machine".

Tasks

  1. Deterministic RNG. splitmix64(u64) -> u64 per the spec:

    x  += 0x9E3779B97F4A7C15
    z   = (x ^ (x >> 30)) * 0xBF58476D1CE4E7B5
    z   = (z ^ (z >> 27)) * 0x94D049BB133111EB
    out =  z ^ (z >> 31)
    

    Every random choice in the simulator (election timeout, delivery delay, partition schedule index) consumes one splitmix64 call on a per-node counter. No language may use its own rand or math/rand or <random> defaults.

  2. Stable iteration. Every map iteration in election, ack tracking, and dump emission is over BTreeMap (Rust), std::map (C++), or a sorted []uint32 (Go). No HashMap / unordered_map / map[uint32] may appear in any code path that affects bytes-on-the-wire or bytes-in-the-dump.

  3. Delivery order. OutMsges enqueued the same tick are delivered in FIFO order per-destination and in source-id ascending order across destinations. Implement with a BinaryHeap<(deliver_at, src_id, seq_no, msg)> (Rust) and the equivalent in Go (container/heap with the same key) and C++ (std::priority_queue). The seq_no tie-breaks duplicates within the same tick.

  4. Partition modelling. --partition a,b,c,d,... is a list of (src, dst) one-way drops. Store as a BTreeSet<(u32, u32)>. At delivery, drop the message if (src, dst) ∈ partition_set. Symmetric partitions are expressed as 0,1,1,0. Single-arg list length must be even (no half-drop); reject odd-length input with exit code 2.

  5. zabctl CLI surface. All three binaries accept:

    zabctl --seed <u64> --nodes <u32> --rounds <u32> --proposals <u32> [--partition a,b,c,d,...]
    

    Print the lowercase-hex sha256 of dump_cluster(...) with no trailing newline. Exit code 2 on any bad flag.

  6. Wire-format magic. First 8 bytes of the dump are the ASCII string "DSEZAB01". Bump to "DSEZAB02" if the layout ever changes (and update docs/observation.md in the same commit).

Acceptance

scripts/cross_test.sh succeeds end-to-end on a clean checkout:

=== ALL OK ===

Each of the six scenarios A–F prints the same hex digest for Rust, Go, and C++. The canonical hashes are pinned in docs/observation.md — if any scenario changes you must update the table in the same commit, with a one-line note on what shifted (timer constant, schedule formula, dump layout).

Optional but valuable: rebuild on a second machine with a different endian-ness-irrelevant compiler (Linux/gcc vs macOS/clang) and confirm the hashes match. All targets in this study back are little-endian; the dump assumes that.