| .forgejo/workflows | ||
| vendor | ||
| app.js | ||
| crypto.js | ||
| index.html | ||
| p2p.js | ||
| protocol.js | ||
| README.md | ||
| style.css | ||
| test-protocol.html | ||
randomp2p
Vertrauensloser Multiplayer-Münzwurf – P2P im Browser, keine Server, kein Trust.
Zwei bis N Spieler verbinden sich via WebRTC (simple-peer) und führen ein kryptografisches Commit-Reveal-Protokoll aus, um einen garantiert fairen Münzwurf zu erhalten. Solange ein Teilnehmer ehrlich ist, ist das Ergebnis zufällig und manipulationssicher.
Features
- P2P – Kein Server, kein Account, keine zentrale Instanz
- Trustless – Commit-Reveal-Protokoll mit SHA-256-Bindung, jeder verifiziert
- QR-Bootstrapping – SDP-Offers/Answers werden als QR-Codes ausgetauscht
- Multiplayer (2–n) – Beliebig viele Teilnehmer in einer Runde
- Full Mesh – Alle Peers verbinden sich direkt per WebRTC (Host relayt nur initial SDP)
- Selective Abort – Timeout erkennt Verweigerer, alle Teilnehmer sehen denselben Schuldigen
- Coin-Animation – 3D-Münzwurf via CSS
- 100% Vanilla JS – Keine Build-Tools, kein npm, keine Abhängigkeiten außer CDN-Libs
Quick Start
git clone <repo-url>
cd randomp2p
python3 -m http.server 8080
Dann auf zwei (oder mehr) Geräten http://localhost:8080 öffnen.
WebRTC + Kamera (
getUserMedia) + Web Crypto API brauchen einen sicheren Kontext. Lokal via localhost funktioniert das. Für andere Geräte im selben Netzwerk die lokale IP verwenden.
Spielanleitung
Host
- "Raum erstellen" – QR-Code mit SDP Offer wird angezeigt
- "Antwort-QR scannen" – Kamera startet, scannt die Antwort-QRs der Beitreter
- Jeder neue Spieler erscheint in der Liste
- Host broadcastet
rostermit allen Peer-IDs → Peers bauen direktes Mesh untereinander auf - Sobald alle Peers
mesh_readygemeldet haben → "Münzwurf starten" wird aktiv - "Münzwurf starten" – Übergang in den Spiel-Screen
- "Münzwurf starten" – Protokoll beginnt (Commit → Reveal → Ergebnis)
Beitreter
- "Raum beitreten" – Kamera startet
- QR des Hosts scannen
- Antwort-QR zeigen – Host scannt diesen QR
- Verbindung steht, empfange
rostermit anderen Peer-IDs - Baue direkte WebRTC-Verbindung zu jedem anderen Peer auf (kleinere PeerId initiiert → kein Glare)
- Sobald alle direkten Verbindungen stehen → automatisch
mesh_readyan Host - Host startet Protokoll → alle laufen automatisch durch (Commit → Reveal → Ergebnis)
Protokoll-Phasen
| Phase | Beschreibung |
|---|---|
| Commit | Jeder Spieler generiert 32 Zufallsbytes und sendet den SHA-256-Hash an alle |
| Warten | Sammle Commits aller Teilnehmer (Timeout 12s) |
| Reveal | Jeder sendet die rohen Zufallsbytes an alle |
| Warten | Sammle Reveals aller Teilnehmer (Timeout 12s) |
| Verify | Jeder prüft: SHA-256(received) == stored_commit |
| Result | XOR aller Secrets → Parity (0=Kopf, 1=Zahl) |
Bei Timeout in einer Wartephase: Selective Abort – ABORTED-Zustand, fehlende Peer-IDs werden angezeigt.
Alle Teilnehmer sehen dieselben fehlenden Peers (Mesh → jeder sieht, wer nicht sendet).
Architektur
randomp2p/
├── index.html – 5 Screens + CDN-Libs
├── style.css – Dark-Theme, Coin-Animation
├── crypto.js – SHA-256, Zufallsbytes, XOR-Combine
├── p2p.js – MeshNet: simple-peer, QR-Signaling, Datenkanal
├── protocol.js – Commit-Reveal State Machine
└── app.js – UI-Routing, Kamera/QR-Scan, Verkabelung
Abhängigkeiten (CDN)
| Library | Zweck |
|---|---|
| simple-peer | WebRTC-Datenkanäle |
| qrcodejs | QR-Code-Generierung |
| jsQR | QR-Code-Scanning per Kamera |
Netzwerk-Topologie
Phase 1 – Stern (QR-Bootstrapping): Host tauscht via QR-Codes SDP Offers/Answers mit jedem Peer aus → jeder Peer ist direkt mit dem Host verbunden.
Phase 2 – Mesh (relayed Signaling):
Sobald ein Peer beitritt, broadcastet der Host ein roster mit allen Peer-IDs an das gesamte Netzwerk.
Jeder Peer baut zu jedem anderen Peer eine direkte WebRTC-Verbindung auf:
- Initiator ist immer der Peer mit der kleineren PeerId (verhindert Glare / Race-Conditions)
- SDP Offers/Answers werden über den Host als Relay ausgetauscht (
signal_relay/signal_relayedvia DataChannel) - Nach erfolgreichem Verbindungsaufbau sendet jeder Peer
mesh_readyan den Host
Ergebnis: Volles Mesh – jeder Peer ist mit jedem direkt verbunden.
Broadcasts erreichen alle Teilnehmer in einem Hop. expectedPeers (für Timeout/Abort) ist auf allen identisch.
Host A ──QR── Peer B Host A ──star── Peer B
Host A ──QR── Peer C → Host A ──star── Peer C
Peer B ──mesh── Peer C (direkt, kein Relay)
Signal-Relay-Detail
B (Initiator, peerId kleiner) Host A C (Responder)
│ │ │
├─ signal_relay(to:C, offer) ──────→│ │
│ ├─ signal_relayed(offer) ──→│
│ │ ├─ SimplePeer(non-init)
│ │ ├─ peer.signal(offer)
│ │ ├─ answer signal
│ │←─ signal_relay(to:B, answer) ─┤
│←── signal_relayed(answer) ────────┤ │
├─ peer.signal(answer) │ │
├─ B–C direkt verbunden! │ ├─ B–C direkt verbunden!
│ │ │
├─ mesh_ready ────────────────────→│←────────────────── mesh_ready ─┤
│ │ readyPeers={B,C} → Button enabled
Warum ist das fair?
Das Commit-Reveal-Protokoll garantiert:
- Keine späte Manipulation: Der SHA-256-Commit bindet jeden Spieler vor dem Reveal an sein Secret (Preimage-Resistenz)
- Keine Absprache nötig: Solange ein Teilnehmer ehrlich Zufallsbytes beisteuert, ist das XOR-Ergebnis zufällig
- Volle Transparenz: Jeder Teilnehmer rechnet lokal alle Prüfungen und das Endergebnis
Mathematisch: result = S_1 ⊕ S_2 ⊕ ... ⊕ S_n. Wenn S_k echt zufällig und vor S_1..S_{k-1}, S_{k+1}..S_n festgelegt wurde, ist result zufällig – unabhängig von allen anderen Secrets.
IPFS-Deployment
ipfs add -r .
# → CID notieren, über IPFS-Gateway aufrufbar
# → Gateway-URL als QR-Code in die App einbauen
Ausblick / TODOs
- Volles Mesh: Peers verbinden sich direkt via relayed signaling
- Selective Abort: Timeout bei Verweigerern
- Reconnect bei Verbindungsabbruch
- Raum-Code als Alternative zum QR-Scan
- TURN-Server-Konfiguration für NAT-Traversal
- PWA-Manifest + ServiceWorker für IPFS-Distribution
Lizenz
MIT