Compare commits
3 commits
bdd85db8a7
...
ab14ac8cc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab14ac8cc3 | ||
|
|
e9ba5b5b46 | ||
|
|
84a423cee5 |
4 changed files with 301 additions and 48 deletions
67
README.md
67
README.md
|
|
@ -10,6 +10,8 @@ Zwei bis N Spieler verbinden sich via WebRTC (simple-peer) und führen ein krypt
|
|||
- **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
|
||||
|
||||
|
|
@ -32,27 +34,35 @@ Dann auf zwei (oder mehr) Geräten `http://localhost:8080` öffnen.
|
|||
1. **"Raum erstellen"** – QR-Code mit SDP Offer wird angezeigt
|
||||
2. **"Antwort-QR scannen"** – Kamera startet, scannt die Antwort-QRs der Beitreter
|
||||
3. Jeder neue Spieler erscheint in der Liste
|
||||
4. **"Münzwurf starten"** – Übergang in den Spiel-Screen
|
||||
5. **"Münzwurf starten"** – Protokoll beginnt (Commit → Reveal → Ergebnis)
|
||||
4. Host broadcastet `roster` mit allen Peer-IDs → Peers bauen direktes Mesh untereinander auf
|
||||
5. Sobald alle Peers `mesh_ready` gemeldet haben → **"Münzwurf starten"** wird aktiv
|
||||
6. **"Münzwurf starten"** – Übergang in den Spiel-Screen
|
||||
7. **"Münzwurf starten"** – Protokoll beginnt (Commit → Reveal → Ergebnis)
|
||||
|
||||
### Beitreter
|
||||
|
||||
1. **"Raum beitreten"** – Kamera startet
|
||||
2. QR des Hosts scannen
|
||||
3. **Antwort-QR zeigen** – Host scannt diesen QR
|
||||
4. Verbindung steht, warten auf Start
|
||||
5. Protokoll läuft automatisch durch – Ergebnis erscheint
|
||||
4. Verbindung steht, empfange `roster` mit anderen Peer-IDs
|
||||
5. Baue direkte WebRTC-Verbindung zu jedem anderen Peer auf (kleinere PeerId initiiert → kein Glare)
|
||||
6. Sobald alle direkten Verbindungen stehen → automatisch `mesh_ready` an Host
|
||||
7. 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 |
|
||||
| **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
|
||||
|
||||
```
|
||||
|
|
@ -75,11 +85,44 @@ randomp2p/
|
|||
|
||||
## Netzwerk-Topologie
|
||||
|
||||
**Best Case (gleiches WiFi):**
|
||||
QR-SDP-Austausch zwischen Host und jedem Peer. Host teilt IP-Liste → potentiell volles Mesh über `signal_relay`.
|
||||
**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.
|
||||
|
||||
**Worst Case (NAT/Internet):**
|
||||
Stern-Topologie über den Host. Protokoll funktioniert trotzdem – die Fairness ist nicht von der Topologie abhängig.
|
||||
**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_relayed` via DataChannel)
|
||||
- Nach erfolgreichem Verbindungsaufbau sendet jeder Peer `mesh_ready` an 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?
|
||||
|
||||
|
|
@ -101,11 +144,11 @@ ipfs add -r .
|
|||
|
||||
## Ausblick / TODOs
|
||||
|
||||
- [ ] Volles Mesh: Peers verbinden sich direkt via relayed signaling
|
||||
- [ ] Timeout + Reconnect bei Verbindungsabbruch
|
||||
- [x] Volles Mesh: Peers verbinden sich direkt via relayed signaling
|
||||
- [x] Selective Abort: Timeout bei Verweigerern
|
||||
- [ ] Reconnect bei Verbindungsabbruch
|
||||
- [ ] Raum-Code als Alternative zum QR-Scan
|
||||
- [ ] TURN-Server-Konfiguration für NAT-Traversal
|
||||
- [ ] Mehrere Runden mit Historie
|
||||
- [ ] PWA-Manifest + ServiceWorker für IPFS-Distribution
|
||||
|
||||
## Lizenz
|
||||
|
|
|
|||
19
app.js
19
app.js
|
|
@ -103,7 +103,8 @@
|
|||
function updatePhases(state) {
|
||||
const map = { IDLE:'ready', STARTING:'ready', COMMITTING:'commit',
|
||||
WAITING_FOR_COMMITS:'commit', REVEALING:'reveal',
|
||||
WAITING_FOR_REVEALS:'reveal', VERIFYING:'verify', COMPLETE:'done' };
|
||||
WAITING_FOR_REVEALS:'reveal', VERIFYING:'verify',
|
||||
COMPLETE:'done', ABORTED:'done' };
|
||||
const active = map[state] || 'ready';
|
||||
let doneSeen = false;
|
||||
['ready','commit','reveal','verify','done'].forEach(p => {
|
||||
|
|
@ -141,12 +142,19 @@
|
|||
$('btn-start-protocol').hidden = true;
|
||||
};
|
||||
protocol.onerror = err => {
|
||||
$('protocol-text').textContent = 'Fehler: ' + err;
|
||||
$('protocol-text').textContent = err;
|
||||
$('result-area').hidden = false;
|
||||
$('result-text').textContent = 'Abgebrochen';
|
||||
$('btn-start-protocol').hidden = true;
|
||||
};
|
||||
|
||||
mesh.ondata = (peerId, msg) => {
|
||||
if (msg.type === 'protocol_start' && protocol && protocol.state === 'IDLE') {
|
||||
if (msg.type === 'protocol_start') {
|
||||
if (protocol) protocol.reset();
|
||||
$('coin-area').hidden = true;
|
||||
$('result-area').hidden = true;
|
||||
$('btn-start-protocol').hidden = true;
|
||||
updatePhases('IDLE');
|
||||
protocol.start();
|
||||
return;
|
||||
}
|
||||
|
|
@ -167,13 +175,14 @@
|
|||
|
||||
mesh.onpeerconnect = pid => {
|
||||
renderPlayers('host-player-list');
|
||||
$('btn-start-game').disabled = mesh.getCount() < 1;
|
||||
};
|
||||
mesh.onpeerdisconnect = () => {
|
||||
renderPlayers('host-player-list');
|
||||
$('btn-start-game').disabled = mesh.getCount() < 1;
|
||||
};
|
||||
mesh.onqrupdate = d => showQR('qr-host', d);
|
||||
mesh.onreadyupdate = (ready, total) => {
|
||||
$('btn-start-game').disabled = ready < total || total === 0;
|
||||
};
|
||||
|
||||
mesh.createRoom();
|
||||
show('host');
|
||||
|
|
|
|||
195
p2p.js
195
p2p.js
|
|
@ -2,17 +2,25 @@ class MeshNet {
|
|||
constructor() {
|
||||
this.myPeerId = generatePeerId();
|
||||
this.connections = new Map();
|
||||
this.isHost = false;
|
||||
this._pendingMeshPeers = new Map();
|
||||
this._pendingPeer = null;
|
||||
this._currentQRData = null;
|
||||
this.isHost = false;
|
||||
this.hostPeerId = null;
|
||||
this.roster = [];
|
||||
this.readyPeers = new Set();
|
||||
this._meshReadySent = false;
|
||||
|
||||
this.onpeerconnect = null;
|
||||
this.onpeerdisconnect = null;
|
||||
this.ondata = null;
|
||||
this.onqrupdate = null;
|
||||
this.onanswerready = null;
|
||||
this.onreadyupdate = null;
|
||||
}
|
||||
|
||||
/* ─── Host: room ─── */
|
||||
|
||||
createRoom() {
|
||||
this.isHost = true;
|
||||
this._createPendingPeer();
|
||||
|
|
@ -25,7 +33,6 @@ class MeshNet {
|
|||
|
||||
const peer = new SimplePeer({ initiator: true, trickle: false });
|
||||
let signalSent = false;
|
||||
let connected = false;
|
||||
|
||||
peer.on('signal', signal => {
|
||||
if (signalSent) return;
|
||||
|
|
@ -35,7 +42,6 @@ class MeshNet {
|
|||
});
|
||||
|
||||
peer.on('connect', () => {
|
||||
connected = true;
|
||||
peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId }));
|
||||
});
|
||||
|
||||
|
|
@ -44,19 +50,14 @@ class MeshNet {
|
|||
const msg = JSON.parse(data.toString());
|
||||
this._handleMessage(msg, peer);
|
||||
} catch (e) {
|
||||
console.error('p2p data parse error:', e);
|
||||
console.error('p2p data error:', e);
|
||||
}
|
||||
});
|
||||
|
||||
peer.on('close', () => {
|
||||
this._cleanupPeer(peer);
|
||||
});
|
||||
peer.on('close', () => this._cleanupPeer(peer));
|
||||
peer.on('error', err => console.error('p2p error:', err));
|
||||
|
||||
peer.on('error', err => {
|
||||
console.error('p2p error:', err);
|
||||
});
|
||||
|
||||
this._pendingPeer = { peer, signalSent, connected, peerId: null };
|
||||
this._pendingPeer = { peer, signalSent, connected: false, peerId: null };
|
||||
}
|
||||
|
||||
feedAnswer(answerStr) {
|
||||
|
|
@ -71,14 +72,16 @@ class MeshNet {
|
|||
}
|
||||
}
|
||||
|
||||
/* ─── Peer: join ─── */
|
||||
|
||||
joinFromQR(qrContent) {
|
||||
const data = JSON.parse(qrContent);
|
||||
const remoteId = data.id;
|
||||
this.hostPeerId = remoteId;
|
||||
const offer = data.s;
|
||||
|
||||
const peer = new SimplePeer({ initiator: false, trickle: false });
|
||||
let answerSent = false;
|
||||
let connected = false;
|
||||
|
||||
peer.on('signal', signal => {
|
||||
if (answerSent) return;
|
||||
|
|
@ -90,7 +93,6 @@ class MeshNet {
|
|||
});
|
||||
|
||||
peer.on('connect', () => {
|
||||
connected = true;
|
||||
this.connections.set(remoteId, peer);
|
||||
peer.send(JSON.stringify({ type: 'identity', peerId: this.myPeerId }));
|
||||
if (this.onpeerconnect) this.onpeerconnect(remoteId);
|
||||
|
|
@ -100,25 +102,123 @@ class MeshNet {
|
|||
try {
|
||||
const msg = JSON.parse(data.toString());
|
||||
this._handleMessage(msg, peer);
|
||||
} catch (e) {
|
||||
console.error('p2p data parse error:', e);
|
||||
}
|
||||
} catch (e) { console.error('p2p data error:', e); }
|
||||
});
|
||||
|
||||
peer.on('close', () => {
|
||||
this._cleanupPeer(peer);
|
||||
});
|
||||
|
||||
peer.on('error', err => {
|
||||
console.error('p2p error:', err);
|
||||
});
|
||||
peer.on('close', () => this._cleanupPeer(peer));
|
||||
peer.on('error', err => console.error('p2p error:', err));
|
||||
|
||||
peer.signal(offer);
|
||||
this.connections.set(remoteId, peer);
|
||||
this._pendingPeer = { peer, peerId: remoteId, connected };
|
||||
this._pendingPeer = { peer, peerId: remoteId, connected: answerSent };
|
||||
}
|
||||
|
||||
/* ─── Mesh helpers ─── */
|
||||
|
||||
_createSimplePeer(initiator, onSignal, onConnect) {
|
||||
const peer = new SimplePeer({ initiator, trickle: false });
|
||||
|
||||
peer.on('signal', signal => {
|
||||
if (onSignal) onSignal(signal);
|
||||
});
|
||||
|
||||
peer.on('connect', () => {
|
||||
if (onConnect) onConnect();
|
||||
// No identity exchange – peerId known from context
|
||||
});
|
||||
|
||||
peer.on('data', data => {
|
||||
try {
|
||||
const msg = JSON.parse(data.toString());
|
||||
this._handleMessage(msg, peer);
|
||||
} catch (e) { console.error('mesh data error:', e); }
|
||||
});
|
||||
|
||||
peer.on('close', () => this._cleanupPeer(peer));
|
||||
peer.on('error', e => console.error('mesh error:', e));
|
||||
|
||||
return peer;
|
||||
}
|
||||
|
||||
_initiateMeshConnection(targetPeerId) {
|
||||
if (this.connections.has(targetPeerId)) return;
|
||||
if (this._pendingMeshPeers.has(targetPeerId)) return;
|
||||
if (this.myPeerId >= targetPeerId) return;
|
||||
|
||||
const peer = this._createSimplePeer(true,
|
||||
signal => {
|
||||
this.sendTo(this.hostPeerId, { type: 'signal_relay', to: targetPeerId, signal });
|
||||
},
|
||||
() => {
|
||||
this.connections.set(targetPeerId, peer);
|
||||
this._pendingMeshPeers.delete(targetPeerId);
|
||||
if (this.onpeerconnect) this.onpeerconnect(targetPeerId);
|
||||
this._checkMeshReady();
|
||||
}
|
||||
);
|
||||
|
||||
this._pendingMeshPeers.set(targetPeerId, peer);
|
||||
}
|
||||
|
||||
_handleRelayedSignal(fromPeerId, signal) {
|
||||
let peer = this.connections.get(fromPeerId) || this._pendingMeshPeers.get(fromPeerId);
|
||||
|
||||
if (!peer) {
|
||||
peer = this._createSimplePeer(false,
|
||||
signal => {
|
||||
this.sendTo(this.hostPeerId, { type: 'signal_relay', to: fromPeerId, signal });
|
||||
},
|
||||
() => {
|
||||
this.connections.set(fromPeerId, peer);
|
||||
this._pendingMeshPeers.delete(fromPeerId);
|
||||
if (this.onpeerconnect) this.onpeerconnect(fromPeerId);
|
||||
this._checkMeshReady();
|
||||
}
|
||||
);
|
||||
this._pendingMeshPeers.set(fromPeerId, peer);
|
||||
}
|
||||
|
||||
peer.signal(signal);
|
||||
}
|
||||
|
||||
_checkMeshConnections() {
|
||||
for (const pid of this.roster) {
|
||||
if (pid === this.myPeerId) continue;
|
||||
if (this.connections.has(pid)) continue;
|
||||
if (this.myPeerId < pid) {
|
||||
this._initiateMeshConnection(pid);
|
||||
}
|
||||
}
|
||||
this._checkMeshReady();
|
||||
}
|
||||
|
||||
_checkMeshReady() {
|
||||
if (this._meshReadySent) return;
|
||||
if (!this.hostPeerId) return;
|
||||
if (!this.connections.has(this.hostPeerId)) return;
|
||||
|
||||
const allConnected = this.roster.every(pid =>
|
||||
pid === this.myPeerId || this.connections.has(pid)
|
||||
);
|
||||
|
||||
if (allConnected && this.roster.length > 0) {
|
||||
this._meshReadySent = true;
|
||||
this.sendTo(this.hostPeerId, { type: 'mesh_ready' });
|
||||
}
|
||||
}
|
||||
|
||||
_broadcastRoster() {
|
||||
this.roster = this.getPeerIds();
|
||||
this.readyPeers.clear();
|
||||
this.broadcast({ type: 'roster', peers: this.roster });
|
||||
if (this.onreadyupdate) this.onreadyupdate(0, this.roster.length);
|
||||
}
|
||||
|
||||
/* ─── Message routing ─── */
|
||||
|
||||
_handleMessage(msg, peer) {
|
||||
const peerId = this._findPeerId(peer) || 'unknown';
|
||||
|
||||
if (msg.type === 'identity') {
|
||||
const existing = this._findPeerId(peer);
|
||||
if (existing && existing !== msg.peerId) {
|
||||
|
|
@ -132,6 +232,7 @@ class MeshNet {
|
|||
if (this.onpeerconnect) this.onpeerconnect(msg.peerId);
|
||||
if (this.isHost) {
|
||||
setTimeout(() => this._createPendingPeer(), 300);
|
||||
this._broadcastRoster();
|
||||
}
|
||||
} else {
|
||||
if (this.onpeerconnect) this.onpeerconnect(msg.peerId);
|
||||
|
|
@ -140,27 +241,51 @@ class MeshNet {
|
|||
}
|
||||
|
||||
if (msg.type === 'signal_relay') {
|
||||
const target = this.connections.get(msg.to);
|
||||
const target = this.connections.get(msg.to) || this._pendingMeshPeers.get(msg.to);
|
||||
if (target) {
|
||||
target.send(JSON.stringify({
|
||||
type: 'signal_relayed',
|
||||
from: msg.from,
|
||||
from: peerId,
|
||||
signal: msg.signal
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'signal_relayed') {
|
||||
this._handleRelayedSignal(msg.from, msg.signal);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'roster') {
|
||||
this.roster = msg.peers || [];
|
||||
this._meshReadySent = false;
|
||||
this._checkMeshConnections();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'mesh_ready' && this.isHost) {
|
||||
this.readyPeers.add(peerId);
|
||||
if (this.onreadyupdate) {
|
||||
this.onreadyupdate(this.readyPeers.size, this.roster.length);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ondata) {
|
||||
const peerId = this._findPeerId(peer) || 'unknown';
|
||||
this.ondata(peerId, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Peer management ─── */
|
||||
|
||||
_findPeerId(peer) {
|
||||
for (const [id, p] of this.connections) {
|
||||
if (p === peer) return id;
|
||||
}
|
||||
for (const [id, p] of this._pendingMeshPeers) {
|
||||
if (p === peer) return id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -170,11 +295,21 @@ class MeshNet {
|
|||
this.connections.delete(peerId);
|
||||
if (this.onpeerdisconnect) this.onpeerdisconnect(peerId);
|
||||
}
|
||||
|
||||
for (const [id, p] of this._pendingMeshPeers) {
|
||||
if (p === peer) {
|
||||
this._pendingMeshPeers.delete(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._pendingPeer && this._pendingPeer.peer === peer) {
|
||||
this._pendingPeer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Send ─── */
|
||||
|
||||
broadcast(msg) {
|
||||
const str = JSON.stringify(msg);
|
||||
for (const [id, peer] of this.connections) {
|
||||
|
|
@ -208,7 +343,11 @@ class MeshNet {
|
|||
for (const peer of this.connections.values()) {
|
||||
try { peer.destroy(); } catch (e) {}
|
||||
}
|
||||
for (const peer of this._pendingMeshPeers.values()) {
|
||||
try { peer.destroy(); } catch (e) {}
|
||||
}
|
||||
this.connections.clear();
|
||||
this._pendingMeshPeers.clear();
|
||||
if (this._pendingPeer) {
|
||||
try { this._pendingPeer.peer.destroy(); } catch (e) {}
|
||||
this._pendingPeer = null;
|
||||
|
|
|
|||
68
protocol.js
68
protocol.js
|
|
@ -7,6 +7,10 @@ class CoinFlipProtocol {
|
|||
this.reveals = new Map();
|
||||
this.result = null;
|
||||
this.totalPeers = 0;
|
||||
this.expectedPeers = new Set();
|
||||
this.TIMEOUT_MS = 12000;
|
||||
this._commitTimer = null;
|
||||
this._revealTimer = null;
|
||||
|
||||
this.onstatechange = null;
|
||||
this.onprogress = null;
|
||||
|
|
@ -17,7 +21,8 @@ class CoinFlipProtocol {
|
|||
async start() {
|
||||
if (this.state !== 'IDLE') return;
|
||||
this.state = 'STARTING';
|
||||
this.totalPeers = this.mesh.getCount() + 1;
|
||||
this.expectedPeers = new Set([this.mesh.myPeerId, ...this.mesh.getPeerIds()]);
|
||||
this.totalPeers = this.expectedPeers.size;
|
||||
this.secret = await generateSecret();
|
||||
const myCommit = await commit(this.secret);
|
||||
this.commits.set(this.mesh.myPeerId, myCommit);
|
||||
|
|
@ -26,6 +31,7 @@ class CoinFlipProtocol {
|
|||
this.mesh.broadcast({ type: 'protocol', phase: 'commit', data: bufToBase64(myCommit) });
|
||||
this.state = 'WAITING_FOR_COMMITS';
|
||||
if (this.onstatechange) this.onstatechange('WAITING_FOR_COMMITS');
|
||||
this._startCommitTimer();
|
||||
this._checkCommits();
|
||||
}
|
||||
|
||||
|
|
@ -49,9 +55,54 @@ class CoinFlipProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
/* ─── Timeout ─── */
|
||||
|
||||
_startCommitTimer() {
|
||||
this._clearCommitTimer();
|
||||
this._commitTimer = setTimeout(() => {
|
||||
if (this.commits.size >= this.totalPeers) return;
|
||||
const missing = [...this.expectedPeers].filter(p => !this.commits.has(p));
|
||||
this._abort(`Timeout auf Commits von: ${missing.map(p => p.slice(0, 8)).join(', ')}`);
|
||||
}, this.TIMEOUT_MS);
|
||||
}
|
||||
|
||||
_startRevealTimer() {
|
||||
this._clearRevealTimer();
|
||||
this._revealTimer = setTimeout(() => {
|
||||
if (this.reveals.size >= this.totalPeers) return;
|
||||
const missing = [...this.expectedPeers].filter(p => !this.reveals.has(p));
|
||||
this._abort(`Timeout auf Reveals von: ${missing.map(p => p.slice(0, 8)).join(', ')}`);
|
||||
}, this.TIMEOUT_MS);
|
||||
}
|
||||
|
||||
_clearCommitTimer() {
|
||||
if (this._commitTimer) {
|
||||
clearTimeout(this._commitTimer);
|
||||
this._commitTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_clearRevealTimer() {
|
||||
if (this._revealTimer) {
|
||||
clearTimeout(this._revealTimer);
|
||||
this._revealTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_abort(reason) {
|
||||
this._clearCommitTimer();
|
||||
this._clearRevealTimer();
|
||||
this.state = 'ABORTED';
|
||||
if (this.onstatechange) this.onstatechange('ABORTED');
|
||||
if (this.onerror) this.onerror(reason);
|
||||
}
|
||||
|
||||
/* ─── Commit phase ─── */
|
||||
|
||||
_checkCommits() {
|
||||
if (this.state !== 'WAITING_FOR_COMMITS') return;
|
||||
if (this.commits.size >= this.totalPeers) {
|
||||
this._clearCommitTimer();
|
||||
this._allCommitsReceived();
|
||||
}
|
||||
}
|
||||
|
|
@ -64,17 +115,23 @@ class CoinFlipProtocol {
|
|||
if (this.onprogress) this.onprogress('reveal', 1, this.totalPeers);
|
||||
this.state = 'WAITING_FOR_REVEALS';
|
||||
if (this.onstatechange) this.onstatechange('WAITING_FOR_REVEALS');
|
||||
this._startRevealTimer();
|
||||
this._checkReveals();
|
||||
}
|
||||
|
||||
/* ─── Reveal phase ─── */
|
||||
|
||||
_checkReveals() {
|
||||
if (this.state !== 'WAITING_FOR_REVEALS') return;
|
||||
if (this.reveals.size >= this.totalPeers) {
|
||||
this._clearRevealTimer();
|
||||
this._allRevealsReceived();
|
||||
}
|
||||
}
|
||||
|
||||
async _allRevealsReceived() {
|
||||
this._clearRevealTimer();
|
||||
|
||||
this.state = 'VERIFYING';
|
||||
if (this.onstatechange) this.onstatechange('VERIFYING');
|
||||
|
||||
|
|
@ -82,12 +139,12 @@ class CoinFlipProtocol {
|
|||
if (peerId === this.mesh.myPeerId) continue;
|
||||
const commitment = this.commits.get(peerId);
|
||||
if (!commitment) {
|
||||
if (this.onerror) this.onerror(`Fehler: Kein Commit von ${peerId}`);
|
||||
this._abort(`Fehler: Kein Commit von ${peerId.slice(0, 8)}`);
|
||||
return;
|
||||
}
|
||||
const expected = await commit(secret);
|
||||
if (!arraysEqual(expected, commitment)) {
|
||||
if (this.onerror) this.onerror(`Betrug erkannt! ${peerId} hat gefälscht.`);
|
||||
this._abort(`Betrug erkannt! ${peerId.slice(0, 8)} hat gefälscht.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -98,12 +155,17 @@ class CoinFlipProtocol {
|
|||
if (this.oncomplete) this.oncomplete(this.result);
|
||||
}
|
||||
|
||||
/* ─── Reset ─── */
|
||||
|
||||
reset() {
|
||||
this._clearCommitTimer();
|
||||
this._clearRevealTimer();
|
||||
this.state = 'IDLE';
|
||||
this.secret = null;
|
||||
this.commits.clear();
|
||||
this.reveals.clear();
|
||||
this.result = null;
|
||||
this.totalPeers = 0;
|
||||
this.expectedPeers = new Set();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue