A peer-to-peer secret-sharing voting system. Nodes form a gossip mesh, voters submit additively-shared ballots, and any node can trigger a distributed tally.
This README covers how to build and run the various parts of the project.
Everything is driven through zig build steps defined in build.zig.
- Zig 0.15.2 (With zig being pre 1.0 no other versions are guarenteed to compile)
- A POSIX system (developed/tested on macOS and Linux).
- Optional: Docker, only for the
dockerstep.
All commands are run from the project root.
| Step | What it does |
|---|---|
zig build |
Compile everything into zig-out/bin/ |
zig build test |
Run all unit tests |
zig build test-integration |
Run all integration tests |
zig build test-all |
Run unit and integration tests |
zig build run-node -- <args> |
Build and launch one node |
zig build run-client -- <args> |
Build and run the client CLI |
zig build run-ballot -- <file> |
Replay one ballot file against a running cluster |
zig build run-eval -- <args> |
Run the evaluation harness (spins up its own cluster) |
zig build eval-quick |
Quick smoke eval |
zig build docker |
Build the votempc-zig Docker image |
zig build clean |
Remove zig-out, caches, and results.json |
Anything after -- is forwarded to the program. To see a binary's own help,
pass --help, e.g. zig build run-client -- --help.
zig build test # all unit testsIndividual modules can be run on their own:
zig build test-field # field.zig (finite-field arithmetic)
zig build test-voting # voting.zig (tally rules)
zig build test-protocol # protocol.zig (framed JSON messaging)
zig build test-node # node.zig (state machine)
zig build test-runtime # node_runtime.zigEach integration test spawns its own real cluster of node processes on loopback ports, exercises it, and tears it down.
zig build test-integration # run all fourOr one at a time:
zig build test-phases # phase-transition enforcement (ports 9171–9174)
zig build test-gossip # gossip set-agreement (ports 9101+)
zig build test-e2e # full vote → tally → node-loss (ports 9101–9105)
zig build test-ballots # replays all three ballot files (ports 9101–9105)zig build test-allIf an integration test fails, kill leftover node processes before re-running. The test harnesses only clean up their child nodes on a clean exit, so a failed run can leave
nodeprocesses bound to its ports. A stale node answering on a reused port produces confusing results (e.g. an election version that's higher than it should be). Clear them with:pkill -f 'zig-out/bin/node' # verify the ports are free, e.g. for test-phases: lsof -nP -iTCP:9171-9174 -sTCP:LISTEN
Start a genesis node (no seeds), then start followers that seed off it.
# Terminal 1 — genesis
zig build run-node -- --id 1 --port 9101
# Terminal 2 — follower
zig build run-node -- --id 2 --port 9102 --seeds 127.0.0.1:9101
# Terminal 3 — follower
zig build run-node -- --id 3 --port 9103 --seeds 127.0.0.1:9101Node flags:
| Flag | Meaning | Default |
|---|---|---|
--id N |
Unique node id (required) | — |
--port P |
Bind port (required) | — |
--host H |
Bind address | 127.0.0.1 |
--advertise-host H |
Address peers use to reach this node | value of --host |
--seeds h:p,h:p |
Comma-separated seed list (empty ⇒ genesis) | empty |
Each node prints its admin token when an election is configured — you need that token to start and close voting.
Once you've built once, you can also run the binary directly:
./zig-out/bin/node --id 1 --port 9101.
The client talks to any node (default 127.0.0.1:9101, or set the NODE env
var, or pass --node host:port).
# 1. Configure the election (run against the creator node)
zig build run-client -- configure --candidates Alice,Bob,Carol --rule plurality --threshold 2
# 2. Open voting (use the admin token printed by the node)
zig build run-client -- start-voting --admin-token <TOKEN>
# 3. Cast votes
zig build run-client -- vote --choice Alice # plurality
zig build run-client -- vote --approve Alice,Bob # approval
zig build run-client -- vote --rank Alice,Bob,Carol # irv (full ranking)
# 4. Inspect / finish
zig build run-client -- status
zig build run-client -- roster
zig build run-client -- close-voting --admin-token <TOKEN>
zig build run-client -- tally
zig build run-client -- resultPoint the client at a specific node with --node:
zig build run-client -- --node 127.0.0.1:9103 statusSupported rules are plurality, approval, and irv. Full usage:
zig build run-client -- --help.
run-ballot drives a complete election from a JSON ballot file against an
already-running cluster: configure → start voting → cast every ballot →
tally → verify the result matches the file's expected outcome.
zig build run-ballot -- ballots/plurality_100.json
zig build run-ballot -- ballots/irv_100.json --tally-wait 15
zig build run-ballot -- ballots/approval_100.json --node 127.0.0.1:9101| Flag | Meaning | Default |
|---|---|---|
--node host:port |
Node to drive | 127.0.0.1:9101 |
--tally-wait SEC |
How long to wait for the tally result | 8 |
--no-verify |
Skip the expected-vs-actual check | off |
(The test-ballots step does this automatically for all three files with a
freshly spawned cluster, so you don't need a running cluster for that one.)
eval builds everything, spins up its own clusters across a sweep of
configurations, and writes timing/traffic results to JSON.
zig build eval-quick # minimal 2-config smoke sweep
zig build run-eval # full sweep, 3 trials/config
zig build run-eval -- --trials 10 # more trials for stable numbers
zig build run-eval -- --out my_results.json| Flag | Meaning | Default |
|---|---|---|
--quick |
Minimal sweep (2 configs) | off |
--trials N |
Trials per configuration | 3 |
--out PATH |
Output JSON path | results.json |
zig build docker # builds image tagged votempc-zigzig build clean # removes zig-out, zig-cache, .zig-cache, results.json