Among-Midnight - Shadow Protocol Documentation

MLH Hackathon · Midnight Gaming Track

Table of Contents


Overview

Among-Midnight is an unofficial Python clone of Among Us augmented with a zero-knowledge anti-cheat layer called Shadow Protocol, built on the Midnight Network.

The base game's multiplayer dealer was literally rigged — the impostor was always the player with the highest player_id. Shadow Protocol eliminates this by binding the role deal, every kill, every vote, and every ejection to ZK commitments on-chain.

"The game still runs in real time — it just can no longer lie about it."


Why ZK Fairness?

Every centralised game server has a fundamental trust problem:

  • Roles can be forged or pre-determined.
  • Kills can be fabricated by non-impostors.
  • Votes can be stuffed or replayed.
  • Match history can be rewritten after the fact.

Shadow Protocol addresses each of these with on-chain zero-knowledge proofs, making cheating cryptographically impossible — not just against the rules.


Architecture

┌────────────────┐  pickle over TCP :4321   ┌───────────────────────┐
│ pygame clients │◄────────────────────────►│  server.py (relay)     │
│ (unchanged UI) │                          └───────────────────────┘
└──────┬─────────┘
       │ HTTP JSON  localhost :5310  (fire-and-forget — midnight_hooks.py)
       ▼
┌────────────────────────────────┐
│ midnight-bridge  (Node 20, TS) │
│  • seat map, roles, salts (RAM)│
│  • tx queue (serialized+retry) │──► proof server (Docker :6300)
│  • ZK proof generation         │
│  • contract client             │──► Midnight testnet (ShadowLedger)
│  • audit dashboard  /audit     │
└────────────────────────────────┘

Key Components

Component Language Role
game.py Python 3.11 Game loop + ZK hook call-sites
midnight_hooks.py Python 3.11 Fire-and-forget queue worker
server.py Python 3.11 Dumb pickle relay over TCP
midnight-bridge/ TypeScript 5.9 Bridge: proofs, contract, dashboard
shadow_ledger.compact Compact On-chain ZK contract (Midnight)

Shadow Ledger Contract

The core fairness logic lives in shadow_ledger.compact.

Game Phases

0  SETUP    →  initGame()             →  1  LIVE
1  LIVE     →  startMeeting()         →  2  MEETING
2  MEETING  →  recordEjection()       →  1  LIVE
2  MEETING  →  endMeetingNoEjection() →  1  LIVE
any         →  declareWinner()        →  3  OVER

Exported Circuits

// ZK showpiece: proves the caller is SABOTEUR without revealing their seat
export circuit recordKill(victim: Uint<8>): []

// One vote per seat per meeting via salted nullifier; voter identity stays private
export circuit recordVote(target: Uint<8>): []

// Ejection must open the day-one commitment — server cannot lie about the role
export circuit recordEjection(seat: Uint<8>, role: Uint<8>, salt: Bytes<32>): []

// Endgame: opens every seat's commitment so anyone can verify the match forever
export circuit auditReveal(seat: Uint<8>, role: Uint<8>, salt: Bytes<32>): []

Commitment Scheme

The commitment binds role + seat + gameSeed so it cannot be replayed for another seat or another match (spec §6.3):

circuit openCommitment(role: Uint<8>, seat: Uint<8>, salt: Bytes<32>): Bytes<32> {
  return persistentCommit<[Field, Field, Bytes<32>]>(
    [role as Field, seat as Field, gameSeed], salt);
}

Trust Model & Cryptographic Guarantees

Threat Base Repo Shadow Protocol v1 Mechanism
Rigged role assignment Rigged by design Impossible Fisher–Yates from on-chain gameSeed, audited at game end
Fake kill by non-saboteur Undetectable Impossible recordKill requires ZK proof of role == SABOTEUR
Vote stuffing / double voting Undetectable Impossible Per-meeting ZK nullifiers in contract state
Lying about ejected role Undetectable Impossible Ejection must open the day-one commitment
Rewriting match history Trivial Impossible Append-only chain record + full audit reveal
Server/bridge reading secrets Yes Still yes — v1 limitation Roadmap: browser clients with Lace wallets

Quickstart

1. Start the Bridge (mock mode — no chain required)

cd midnight-bridge
npm install
npm run dev          # boots on http://127.0.0.1:5310  (MN_MODE=mock)

Open the audit dashboard: http://127.0.0.1:5310/audit

MN_MODE=mock runs an in-process simulated ledger that enforces the same circuit checks as the Compact contract — commitment openings, nullifiers, phase guards. The full game + dashboard work with no chain.

2. Start the Game (Python 3.11)

python3.11 -m venv .venv
.venv\Scripts\activate            # Windows
pip install -r requirements.txt
python server.py                  # LAN relay on :4321
python main.py                    # one per player; choose 'Local'

3. Real Midnight Testnet (Phase 2)

# Compact toolchain
compact update && compact compile --version   # pin version into the contract pragma

# Proof server
docker compose -f proof-server-local.yml up -d

# Compile contract + bindings
cd midnight-bridge && npm run build:chain

Set MN_MODE=testnet in midnight-bridge/.env and fund your deployer address with tDUST from the faucet. Do this on Day 1 — faucet/latency problems are the #1 Midnight hackathon killer.

4. Run Tests

cd midnight-bridge && npm test
# 14 passing: B1–B8 + C-series analogues

HTTP API

All endpoints are served by the midnight-bridge on localhost:5310.

Route Method Behaviour
/health GET Liveness + deps + queue depth + session state
/deal POST Synchronous — anchors seed + commitments on-chain, returns seatMap + saboteurPlayerId
/kill POST 202 enqueue; 409 NOT_SABOTEUR / DEAD_*, 422 self-kill
/vote POST 202; first vote of a round auto-opens the meeting; 409 DUPLICATE
/eject POST 202 (ejectedId: null = no ejection); 422 stale round
/gameover POST 202; queues declareWinner + 5× auditReveal, writes audit_log.json
/reset POST Dev only — clears RAM session
/events GET SSE stream of audit events (dashboard feed)
/audit GET Judge-facing dashboard
/audit/data GET Verification payload — reveals: [] until OVER; no role/salt leaks early

Math Behind the Commitments

Role Assignment — Fisher–Yates Shuffle

The saboteur seat is selected via a deterministic Fisher–Yates shuffle seeded by the on-chain gameSeed. For \( n \) players, the shuffle runs in \( O(n) \) and produces a uniformly random permutation.

The probability that any specific player is assigned the Saboteur role is:

$$ P(\text{seat } i = \text{SABOTEUR}) = \frac{1}{n} $$

With \( n = 5 \) (the spec-locked default), each player has a 20% chance of being the Saboteur.

Vote Nullifier

To prevent double-voting, a per-seat, per-meeting nullifier is computed as:

$$ \text{nullifier} = H!\left(\,\text{salt} \;|\; \text{meetingRound} \;|\; \texttt{"vote"}\,\right) $$

where \( H \) is the Midnight persistentHash function. The nullifier is stored in the on-chain voteNullifiers set. A second vote from the same seat recomputes the same nullifier and fails the assert(!voteNullifiers.member(...)) check.


What Was Changed in the Game

All patches are tagged # [MIDNIGHT] in source.

Patch File What
P1 Deal game.py Rigged "highest player_id" dealer deleted; roles come only from bridge /deal
P2 Kill game.py Killer's client posts /kill after the existing kill handling
P3 Vote game.py Voter's client posts /vote once per meeting
P4 Eject game.py Ejected client posts /eject; bridge dedupes by round
P5 Game over game.py Each win branch posts /gameover
midnight_hooks.py The only new Python file: fire-and-forget queue worker, 5s timeouts, capped backoff

No pickle payload shapes, sprites, screens, sounds, or menus were touched.


Known Limitations

  • Bridge custody — the bridge holds all salts, so it can generate any seat's proofs. The chain still prevents anything inconsistent with the day-one commitments (an AGENT seat can never produce a valid kill proof).
  • Pickle over LAN — inherited from the base repo; trusted-LAN demo only.
  • Task-completion wins — attested by the game; the contract cannot see tasks (but it still enforces saboteur ejected ⇒ agents win).
  • Bridge crash mid-match — pending events are lost (RAM state); audit_log.json has everything up to the last write.

Roadmap

  • Web client with player-held salts + Lace wallets
  • Mental-poker trustless dealing (removes bridge custody)
  • Per-seat wallets

Screenshots

Menu screen

In-game view

Voting screen


Credits

  • InnerslothAmong Us game and assets
  • Midnight Network — ZK smart-contract platform
  • All Midnight integration built during the MLH hackathon

See also: SHADOW_PROTOCOL.md · DEMO_GUIDE.md

Built With

Share this project:

Updates