The correct build
CI runs the 20,000-seed hunt and the mutant table on every push to main.
The correct build also passed 2,000 seeds in 12.9 s and the 5,000 seeds inside quorumlab mutants, which took 16.2 s with all six bugs.
Raft, written from scratch in Go, and a deterministic simulator that attacks it. The simulator partitions the network, crashes and restarts nodes, drops, duplicates and reorders messages, and checks Raft's safety properties after every event. Any failure replays exactly from its seed.
A distributed system rarely fails on the happy path. It fails when a leader dies halfway through replicating an entry, or a restarted node forgets a vote it already cast. A clean run only means something if the checker would have noticed a broken one, so the repository also carries six known ways to get Raft wrong, and the simulator has to catch every one.
Plays the six stages below in order, about 60 seconds. Scroll or press Esc to stop.
All three are real runs of quorumlab 0.1.0 built from commit 4e365b0, output verbatim. Hunt timings are wall-clock on a 12-core Linux machine; everything else is simulated time and identical on any machine.
raft.Node doesn't send anything, sleep or read the time. Time arrives as Tick(), messages as Step(m) and client commands as Propose(data). Everything the node wants done comes back from Ready(): state to persist, messages to send, committed entries to apply.
Not implemented: membership changes, snapshots, PreVote and leader leases. The cluster is fixed and the log grows forever.
Three nodes, driven by hand Illustrative input
put x 1 were made up for this page: a short driver outside the repository builds three nodes with the repo's own raft package, delivers every message in order and prints each input and each Ready. Every line below the input is what the nodes returned, verbatim.One event queue and one virtual clock that jumps from event to event. Every message gets a random delay, and 5% take a slow path that lets later traffic overtake them. Messages are dropped and duplicated, each node ticks at its own rate, and nodes crash, losing everything they hadn't persisted.
The README records that two of the six bugs went uncaught in 5,000 seeds until bridges and rings were added.
The four shapes, as seed 2 drew them
quorumlab run --seed 2 --trace, with their times. Links are drawn from the rules in sim/sim.go, and the shading follows the same majority count the simulator uses for its timeline.Four clients each run 60 commands (get, put and append) on three keys. They retry with the same sequence number when a reply doesn't come, and follow leader hints. After 10 simulated seconds the chaos phase ends: the network heals and becomes fast, every node comes back, and the clients get 10 more seconds to finish.
TestSameSeedSameRun runs seeds twice and compares the entire result.Seed 2, the correct build, start to finish
--svg, dumped as JSON by a driver outside the repository. The log lines are --trace output and the counts are the run's summary, verbatim. Played back about twice as fast as simulated time.On the live cluster, after every event: at most one leader per term, logs that match wherever they share an entry, every new leader holding the entries committed before its term, no two nodes applying different entries at one index, terms and commit indexes that never go back, and no panics.
The end-of-run checks see what the per-event ones can't. Seed 2 with one bug switched on, applying a retried command twice, runs every one of the same events and passes every per-event check.
Seed 2, twice: the same events, one different ending
Correct build
--bug no-dedup
quorumlab run --seed 2, without and with --bug no-dedup; the bug's run then printed its last 12 trace lines, left out here. The check list restates the README's two tables, which match the checks in sim/sim.go.ClearedAbove this line the correct build is attacked and holds. Below it, the same checker meets six deliberate bugs, and each one has to be caught and replayed from its seed.
Each bug is a real mistake people make implementing Raft, kept behind a switch in raft.Bugs, or NoDedup for the state machine. quorumlab mutants hunts the correct build and every bug, and fails if the correct build breaks or any bug gets away.
forget-vote is the hardest. A node must vote, crash, restart and vote again for a rival in the same term, and the rival must still win. That usually takes a bridge partition.TestEveryBugIsCaught fails if any bug stops being found within its bound: 600 and 1,500 seeds for the two rare bugs, first caught at seeds 258 and 674, and 50 for the other four, which are caught within the first six.TestFigure8 replays Figure 8 of the paper message by message, with and without the bug.A clean hunt is evidence, not proof. The mutants show the search has teeth, not that it reaches every bug.
quorumlab mutants, then each bug over 3,000 seeds
quorumlab mutants, which runs 5,000 seeds of the correct build. The bars are from quorumlab hunt --bug NAME --seeds 3000, one run per bug; all six counts match the README's table. Descriptions are from quorumlab bugs.With commit-previous-term switched on, the hunt first fails at seed 258. The leader of term 50 counts replicas of the term-19 leader's entry at index 52 and calls it committed. Node 2, holding the term-20 leader's entry at that index, then wins term 51 and would overwrite it.
hunt on a 12-core machine replays identically on a laptop with quorumlab run --seed N --bug NAME --trace.Seed 258 with the bug, 3.0 to 4.8 s
--trace output, run 112c7df704b401337…12c7df704b401337…docs/figure8.svg from the same seed also left the committed file byte for byte unchanged.Every number here was re-run from a clean clone of commit 4e365b0 with Go 1.27 on one 12-core Linux machine. The seed counts are deterministic and match the README; the times are this machine's.
CI runs the 20,000-seed hunt and the mutant table on every push to main.
The correct build also passed 2,000 seeds in 12.9 s and the 5,000 seeds inside quorumlab mutants, which took 16.2 s with all six bugs.
The first seed that fails, out of seeds tried in order from 1.
The other two bugs fail on seed 1. forget-vote broke only 3 of 3,000 seeds, the Figure 8 bug 29.
The README records two mistakes in the harness, both fixed. The first Leader Completeness check was too strict: a ring partition produced a legal run where a node won an old term after a later term had committed entries.
The second was a message storm. Under duplication, one run of keep-conflicts grew to seven million messages in 20 simulated seconds, until the leader probed again only when a rejection moved its estimate backwards.
Counted with go test -count=1 -json ./...: 32 tests plus 6 subtests, all passing. The README counts the 32 top-level tests; the subtests are one per deliberate bug.
CI runs vet, gofmt, the suite on Go 1.22 and stable, the short suite under -race, the 20,000-seed hunt with the mutant table, and a check that the pictures in docs/ are what the current code draws.
git clone https://github.com/ahmed-hashim-pro/quorum-lab cd quorum-lab go build -o quorumlab ./cmd/quorumlab ./quorumlab run --seed 2 --trace # every election, fault and restart ./quorumlab hunt --seeds 2000 # many seeds, on every core ./quorumlab mutants # the six bugs, each caught ./quorumlab run --seed 258 --bug commit-previous-term --trace go test ./...
Go 1.22 or later and nothing else: no dependencies outside the standard library. make check adds the full 20,000-seed hunt and the mutant table, as CI does. Exit codes: 0 no violation, 1 a violation, 2 a usage error.