diff --git a/app.js b/app.js index 5a8f4a1..c457b5b 100644 --- a/app.js +++ b/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; } diff --git a/protocol.js b/protocol.js index 8abf6a8..8f6baa4 100644 --- a/protocol.js +++ b/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(); } }