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:

flagdefaultmeaning
--seed N0 (Go) / 42 (Rust) / 0 (C++)splitmix64 seed mixed into election timers and message delays
--nodes K3number of ZAB nodes (1 is legal; majority is then 1)
--rounds R0/1000number of simulator ticks to run
--proposals P0number of client commands to inject during the run
--partition s,d,...nonecomma-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.

labelargs
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 last Commit every 50 ticks.
  • ELECTION_TIMEOUT_MIN = 150, ELECTION_TIMEOUT_SPAN = 150 — base + jitter for follower election deadline.
  • DELIVERY_DELAY_SPAN = 3 — message delivery delay is 1 + splitmix64(seed ^ src ^ dst ^ t) % 3 ticks.

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.