class CoinFlipProtocol { constructor(mesh) { this.mesh = mesh; this.state = 'IDLE'; this.secret = null; this.commits = new Map(); 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; this.oncomplete = null; this.onerror = null; } async start() { if (this.state !== 'IDLE') return; this.state = 'STARTING'; 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); if (this.onprogress) this.onprogress('commit', 1, this.totalPeers); if (this.onstatechange) this.onstatechange('COMMITTING'); 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(); } handleMessage(peerId, msg) { if (msg.type !== 'protocol') return; if (msg.phase === 'commit' && (this.state === 'WAITING_FOR_COMMITS' || this.state === 'COMMITTING' || this.state === 'STARTING')) { if (!this.commits.has(peerId)) { this.commits.set(peerId, base64ToBuf(msg.data)); if (this.onprogress) this.onprogress('commit', this.commits.size, this.totalPeers); this._checkCommits(); } } if (msg.phase === 'reveal' && (this.state === 'WAITING_FOR_REVEALS' || this.state === 'REVEALING')) { if (!this.reveals.has(peerId)) { this.reveals.set(peerId, base64ToBuf(msg.data)); if (this.onprogress) this.onprogress('reveal', this.reveals.size, this.totalPeers); this._checkReveals(); } } } /* ─── 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(); } } async _allCommitsReceived() { this.state = 'REVEALING'; if (this.onstatechange) this.onstatechange('REVEALING'); this.mesh.broadcast({ type: 'protocol', phase: 'reveal', data: bufToBase64(this.secret) }); this.reveals.set(this.mesh.myPeerId, this.secret); 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'); for (const [peerId, secret] of this.reveals) { if (peerId === this.mesh.myPeerId) continue; const commitment = this.commits.get(peerId); if (!commitment) { this._abort(`Fehler: Kein Commit von ${peerId.slice(0, 8)}`); return; } const expected = await commit(secret); if (!arraysEqual(expected, commitment)) { this._abort(`Betrug erkannt! ${peerId.slice(0, 8)} hat gefälscht.`); return; } } this.state = 'COMPLETE'; this.result = combine(Array.from(this.reveals.values())); if (this.onstatechange) this.onstatechange('COMPLETE'); 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(); } }