db-19 — Execution
One-shot: prove the lab works
cd db-19-zab
./scripts/verify.sh # all unit tests in Rust, Go, C++
./scripts/cross_test.sh # byte-identical sha256 across all three, six scenarios
A green run of cross_test.sh ends with the literal line:
=== ALL OK ===
Per-language workflows
Rust
cd src/rust
cargo test --release # ~10 tests
cargo build --release # produces target/release/zabctl
./target/release/zabctl --seed 42 --nodes 3 --rounds 1000 --proposals 5
Go
cd src/go
go test ./... # ~9 tests
go build -o /tmp/zabctl_go ./cmd/zabctl
/tmp/zabctl_go --seed 42 --nodes 3 --rounds 1000 --proposals 5
C++
cd src/cpp
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build # test_db19 — 10 assertions
./build/zabctl --seed 42 --nodes 3 --rounds 1000 --proposals 5
CLI
All three binaries accept the same flags and print lowercase hex sha256 of the canonical dump to stdout with no trailing newline:
| flag | default | meaning |
|---|---|---|
--seed N | 0 (Go) / 42 (Rust) / 0 (C++) | splitmix64 seed mixed into election timers and message delays |
--nodes K | 3 | number of ZAB nodes (1 is legal; majority is then 1) |
--rounds R | 0/1000 | number of simulator ticks to run |
--proposals P | 0 | number of client commands to inject during the run |
--partition s,d,... | none | comma-separated pairs (src, dst) to drop in that direction |
(Flag defaults drift between langs because the cross-test script always passes every flag explicitly. Only behavior under explicit flags is part of the cross-language contract.)
--partition 0,1,1,0 drops both directions between nodes 0 and 1
(complete split); --partition 0,1 drops only 0 → 1 (asymmetric).
Proposals are spaced as schedule[i] = (i+1) * rounds / (K+1); with
--rounds 1000 --proposals 5 they fire at ticks 166, 333, 500, 666,
833 with payloads "zab-0" through "zab-4".
Canonical scenarios
scripts/cross_test.sh runs six scenarios; their sha256s are listed
in docs/observation.md. If any change, cross_test.sh will exit
non-zero.
| label | args |
|---|---|
| A | --seed 42 --nodes 3 --rounds 1000 --proposals 5 |
| B | --seed 7 --nodes 5 --rounds 2000 --proposals 20 |
| C | --seed 99 --nodes 3 --rounds 500 --proposals 0 |
| D | --seed 1 --nodes 1 --rounds 200 --proposals 5 |
| E | --seed 42 --nodes 3 --rounds 1000 --proposals 3 --partition 0,1,0,2,1,0,2,0 |
| F | --seed 3 --nodes 5 --rounds 1500 --proposals 10 --partition 0,1 |
D exercises the single-node-leader code path that motivated the
propose() → try_commit() call. E isolates node 0 completely; the
other two must elect a leader and commit the remaining proposals
(the surviving quorum's history is what ends up in node 1 and 2's
dump). F is an asymmetric partition that causes term churn but
recoverable replication.
Sanity checks
# Pick any scenario and round-trip — the hash is content-defined.
./src/rust/target/release/zabctl --seed 42 --nodes 3 --rounds 1000 --proposals 5
# expect: 16af5aa6dbd5ce09b259755f3339d6cf23966ce115b0e30d9c2990487783047d
# Magic of the canonical dump (use the lib directly; the CLI hashes it):
# - Rust: TestDumpDeterministicAcrossRuns asserts da.starts_with("DSEZAB01").
# - Go: TestDumpDeterministicAndMagic asserts the same.
# - C++: test_dump_deterministic_and_magic in tests/test_db19.cc.
Tunables (CONCEPTS.md cross-reference)
HEARTBEAT_INTERVAL = 50— leader re-broadcasts lastCommitevery 50 ticks.ELECTION_TIMEOUT_MIN = 150,ELECTION_TIMEOUT_SPAN = 150— base + jitter for follower election deadline.DELIVERY_DELAY_SPAN = 3— message delivery delay is1 + splitmix64(seed ^ src ^ dst ^ t) % 3ticks.
Changing any of these changes every canonical hash. The intent is that the lab is a fixed-point study object: the values are part of the contract.