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
-
Deterministic RNG.
splitmix64(u64) -> u64per 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
splitmix64call on a per-node counter. No language may use its ownrandormath/randor<random>defaults. -
Stable iteration. Every map iteration in election, ack tracking, and dump emission is over
BTreeMap(Rust),std::map(C++), or a sorted[]uint32(Go). NoHashMap/unordered_map/map[uint32]may appear in any code path that affects bytes-on-the-wire or bytes-in-the-dump. -
Delivery order.
OutMsges enqueued the same tick are delivered in FIFO order per-destination and in source-id ascending order across destinations. Implement with aBinaryHeap<(deliver_at, src_id, seq_no, msg)>(Rust) and the equivalent in Go (container/heapwith the same key) and C++ (std::priority_queue). Theseq_notie-breaks duplicates within the same tick. -
Partition modelling.
--partition a,b,c,d,...is a list of(src, dst)one-way drops. Store as aBTreeSet<(u32, u32)>. At delivery, drop the message if(src, dst) ∈ partition_set. Symmetric partitions are expressed as0,1,1,0. Single-arg list length must be even (no half-drop); reject odd-length input with exit code 2. -
zabctlCLI 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. -
Wire-format magic. First 8 bytes of the dump are the ASCII string
"DSEZAB01". Bump to"DSEZAB02"if the layout ever changes (and updatedocs/observation.mdin 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.