randomp2p/protocol.js
Ole fae9393e8c
All checks were successful
Pin to IPFS / pin (push) Successful in 11s
Gameplay: Nochmal-Synchronisation + Echtzeit-Commit-Zähler + Host-Button umbenannt
- Host-Lobby: 'Münzwurf starten' → 'Spiel starten'
- 'Nochmal': warten bis alle bereit (ready_next/ready_next_all), erst
  dann 'Münzwurf starten' anzeigen
- Progress zeigt Commits/Reveals in Echtzeit (auch vor eigenem Start)
- totalPeers bereits im Konstruktor setzen für sofortige Anzeige
2026-06-14 22:40:15 +02:00

181 lines
5.5 KiB
JavaScript

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 = 1 + mesh.getPeerIds().length;
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', this.commits.size, 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') {
if (!this.commits.has(peerId)) {
this.commits.set(peerId, base64ToBuf(msg.data));
if (this.onprogress)
this.onprogress('commit', this.commits.size, this.totalPeers);
if (this.state === 'WAITING_FOR_COMMITS') this._checkCommits();
}
return;
}
if (msg.phase === 'reveal') {
if (!this.reveals.has(peerId)) {
this.reveals.set(peerId, base64ToBuf(msg.data));
if (this.onprogress)
this.onprogress('reveal', this.reveals.size, this.totalPeers);
if (this.state === 'WAITING_FOR_REVEALS') this._checkReveals();
}
return;
}
}
/* ─── 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', this.reveals.size, 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;
}
}
const sharedSeed = combineAll(Array.from(this.reveals.values()));
const results = new Map();
for (const pid of this.expectedPeers) {
results.set(pid, await computePeerResult(sharedSeed, pid));
}
this.state = 'COMPLETE';
this.results = results;
if (this.onstatechange) this.onstatechange('COMPLETE');
if (this.oncomplete) this.oncomplete(results);
}
/* ─── 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();
}
}