271 lines
11 KiB
HTML
271 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>randomp2p – Protokoll-Test</title>
|
||
<style>
|
||
* { box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; background: #0d1117; color: #c9d1d9; padding: 24px; max-width: 720px; margin: 0 auto; }
|
||
h1 { color: #f0c040; font-size: 1.4rem; margin-bottom: 4px; }
|
||
.sub { color: #8b949e; font-size: 0.85rem; margin-bottom: 20px; }
|
||
button { padding: 10px 24px; background: #238636; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 1rem; font-weight: 600; }
|
||
button:hover { background: #2ea043; }
|
||
button:disabled { opacity: 0.5; cursor: wait; }
|
||
.test { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 14px; margin: 12px 0; }
|
||
.test h3 { margin: 0 0 6px; font-size: 1rem; }
|
||
.test .status { font-weight: 600; }
|
||
.test .detail { font-size: 0.85rem; color: #8b949e; white-space: pre-wrap; margin-top: 4px; }
|
||
.pass { color: #2ea043; }
|
||
.fail { color: #f85149; }
|
||
.running { color: #f0c040; }
|
||
.summary { font-size: 1.1rem; margin: 16px 0; padding: 12px; border-radius: 8px; background: #161b22; border: 1px solid #30363d; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>randomp2p Protokoll-Test</h1>
|
||
<p class="sub">Mock-Mesh statt WebRTC – testet Commit-Reveal isoliert</p>
|
||
<button id="run">Tests ausführen</button>
|
||
<div id="summary" class="summary" style="display:none"></div>
|
||
<div id="output"></div>
|
||
|
||
<script src="crypto.js"></script>
|
||
<script src="protocol.js"></script>
|
||
<script>
|
||
(function() {
|
||
'use strict';
|
||
|
||
const $ = id => document.getElementById(id);
|
||
|
||
class MockMesh {
|
||
constructor(id) {
|
||
this.myPeerId = id;
|
||
this.protocol = null;
|
||
this._others = [];
|
||
this._outbox = [];
|
||
}
|
||
getPeerIds() { return this._others.map(m => m.myPeerId); }
|
||
broadcast(msg) { this._outbox.push({ msg, to: null }); }
|
||
sendTo(peerId, msg) { this._outbox.push({ msg, to: peerId }); }
|
||
flush() {
|
||
const batch = this._outbox.splice(0);
|
||
const deliveries = [];
|
||
for (const item of batch) {
|
||
if (item.to) {
|
||
const target = this._others.find(m => m.myPeerId === item.to);
|
||
if (target) deliveries.push(() => target.protocol.handleMessage(this.myPeerId, item.msg));
|
||
} else {
|
||
for (const other of this._others) {
|
||
deliveries.push(() => other.protocol.handleMessage(this.myPeerId, item.msg));
|
||
}
|
||
}
|
||
}
|
||
for (const fn of deliveries) fn();
|
||
}
|
||
static async createGroup(count) {
|
||
const meshes = [];
|
||
for (let i = 0; i < count; i++) meshes.push(new MockMesh(`peer${i}`));
|
||
for (const m of meshes) {
|
||
m._others = meshes.filter(x => x !== m);
|
||
m.protocol = new CoinFlipProtocol(m);
|
||
}
|
||
return meshes;
|
||
}
|
||
}
|
||
|
||
async function flushAll(meshes) {
|
||
for (const m of meshes) m.flush();
|
||
}
|
||
|
||
function stateLabel(mesh) {
|
||
const map = { IDLE: 'IDLE', STARTING: 'START', COMMITTING: 'COMMIT',
|
||
WAITING_FOR_COMMITS: 'warte(C)', REVEALING: 'REVEAL',
|
||
WAITING_FOR_REVEALS: 'warte(R)', VERIFYING: 'VERIFY', COMPLETE: 'OK', ABORTED: 'ABORT' };
|
||
const s = map[mesh.protocol.state] || mesh.protocol.state;
|
||
return mesh.protocol.state === 'COMPLETE' ? `${s} → ${mesh.protocol.result}` : s;
|
||
}
|
||
|
||
function formatOutcomes(meshes, results, errors, extra) {
|
||
const lines = [];
|
||
for (const m of meshes) {
|
||
const err = errors.find(e => e.peerId === m.myPeerId);
|
||
const res = results.find(r => r.peerId === m.myPeerId);
|
||
const label = extra?.cheaterId === m.myPeerId ? ' (Cheater)' : '';
|
||
if (err) lines.push(` ${m.myPeerId}${label}: ABORT → ${err.error}`);
|
||
else if (res) lines.push(` ${m.myPeerId}${label}: OK → ${res.result}`);
|
||
else lines.push(` ${m.myPeerId}${label}: ${stateLabel(m)}`);
|
||
}
|
||
if (extra?.note) lines.push(` ${extra.note}`);
|
||
return lines.join('\n');
|
||
}
|
||
|
||
async function runHappyPath(count) {
|
||
const meshes = await MockMesh.createGroup(count);
|
||
const results = [], errors = [];
|
||
for (const m of meshes) {
|
||
m.protocol.TIMEOUT_MS = 3000;
|
||
m.protocol.oncomplete = r => results.push({ peerId: m.myPeerId, result: r });
|
||
m.protocol.onerror = e => errors.push({ peerId: m.myPeerId, error: e });
|
||
}
|
||
await Promise.all(meshes.map(m => m.protocol.start()));
|
||
await flushAll(meshes);
|
||
await new Promise(r => setTimeout(r, 10));
|
||
await flushAll(meshes);
|
||
await new Promise(r => setTimeout(r, 10));
|
||
const allSame = results.length === count && results.every(r => r.result === results[0].result);
|
||
return { pass: allSame && errors.length === 0, detail: formatOutcomes(meshes, results, errors) };
|
||
}
|
||
|
||
async function runTimeoutTest() {
|
||
const meshes = await MockMesh.createGroup(3);
|
||
const results = [], errors = [];
|
||
for (const m of meshes) {
|
||
m.protocol.TIMEOUT_MS = 600;
|
||
m.protocol.oncomplete = r => results.push({ peerId: m.myPeerId, result: r });
|
||
m.protocol.onerror = e => errors.push({ peerId: m.myPeerId, error: e });
|
||
}
|
||
await Promise.all(meshes.slice(0, 2).map(m => m.protocol.start()));
|
||
await new Promise(r => setTimeout(r, 20));
|
||
await flushAll(meshes);
|
||
await new Promise(r => setTimeout(r, 20));
|
||
await flushAll(meshes);
|
||
await new Promise(r => setTimeout(r, 800));
|
||
const pass = errors.length >= 2 && errors.every(e => e.error.includes('Timeout'));
|
||
return { pass, detail: formatOutcomes(meshes, results, errors, { note: 'peer2 wurde nie gestartet (simuliert Ausfall)' }) };
|
||
}
|
||
|
||
async function runCheatTest() {
|
||
const meshes = await MockMesh.createGroup(2);
|
||
const results = [], errors = [];
|
||
for (const m of meshes) {
|
||
m.protocol.TIMEOUT_MS = 3000;
|
||
m.protocol.oncomplete = r => results.push({ peerId: m.myPeerId, result: r });
|
||
m.protocol.onerror = e => errors.push({ peerId: m.myPeerId, error: e });
|
||
}
|
||
await Promise.all(meshes.map(m => m.protocol.start()));
|
||
await flushAll(meshes);
|
||
await new Promise(r => setTimeout(r, 10));
|
||
|
||
const fakeSecret = new Uint8Array(32);
|
||
crypto.getRandomValues(fakeSecret);
|
||
for (const m of meshes) {
|
||
if (m.myPeerId !== 'peer0') {
|
||
m.protocol.handleMessage('peer0', { type: 'protocol', phase: 'reveal', data: bufToBase64(fakeSecret) });
|
||
}
|
||
}
|
||
await flushAll(meshes);
|
||
await new Promise(r => setTimeout(r, 10));
|
||
|
||
const victimErrors = errors.filter(e => e.peerId !== 'peer0');
|
||
const pass = victimErrors.length > 0 && victimErrors.every(e => e.error.includes('Betrug'));
|
||
return { pass, detail: formatOutcomes(meshes, results, errors, { cheaterId: 'peer0', note: 'peer0 sandte abweichendes Secret im Reveal' }) };
|
||
}
|
||
|
||
async function runSelectiveAbort(trials = 10) {
|
||
let revealedCount = 0, withheldCount = 0;
|
||
|
||
for (let trial = 0; trial < trials; trial++) {
|
||
const meshes = await MockMesh.createGroup(3);
|
||
const results = [], errors = [];
|
||
const cheater = meshes[0];
|
||
|
||
for (const m of meshes) {
|
||
m.protocol.TIMEOUT_MS = 1200;
|
||
m.protocol.oncomplete = r => results.push({ peerId: m.myPeerId, result: r });
|
||
m.protocol.onerror = e => errors.push({ peerId: m.myPeerId, error: e });
|
||
}
|
||
|
||
await Promise.all(meshes.map(m => m.protocol.start()));
|
||
await flushAll(meshes);
|
||
await new Promise(r => setTimeout(r, 10));
|
||
|
||
const savedReveal = cheater._outbox.splice(0);
|
||
for (const m of meshes.slice(1)) m.flush();
|
||
await new Promise(r => setTimeout(r, 10));
|
||
|
||
const allSecrets = [cheater.protocol.secret];
|
||
for (const m of meshes.slice(1)) {
|
||
const s = cheater.protocol.reveals.get(m.myPeerId);
|
||
if (s) allSecrets.push(s);
|
||
}
|
||
const peeked = combine(allSecrets);
|
||
|
||
const wants = Math.random() < 0.5 ? 0 : 1;
|
||
const revealed = wants === peeked;
|
||
|
||
if (revealed) {
|
||
cheater._outbox.push(...savedReveal);
|
||
cheater.flush();
|
||
await new Promise(r => setTimeout(r, 10));
|
||
revealedCount++;
|
||
} else {
|
||
await new Promise(r => setTimeout(r, 1800));
|
||
withheldCount++;
|
||
}
|
||
|
||
for (const m of meshes) m.protocol.reset();
|
||
}
|
||
|
||
const pass = true;
|
||
return { pass, detail: `${revealedCount}x gerevealt (weil Wunsch == Resultat)\n${withheldCount}x zurückgehalten (weil Wunsch != Resultat)` };
|
||
}
|
||
|
||
function addResult(id, label, status, detail) {
|
||
const div = document.createElement('div');
|
||
div.className = 'test';
|
||
div.id = id;
|
||
div.innerHTML = `<h3>${label}</h3><div class="status ${status}">${status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'RUNNING'}</div><div class="detail">${detail || ''}</div>`;
|
||
$('output').appendChild(div);
|
||
}
|
||
|
||
function updateResult(id, status, detail) {
|
||
const div = document.getElementById(id);
|
||
if (div) {
|
||
div.querySelector('.status').className = 'status ' + status;
|
||
div.querySelector('.status').textContent = status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'RUNNING';
|
||
if (detail !== undefined) div.querySelector('.detail').textContent = detail;
|
||
}
|
||
}
|
||
|
||
async function runTests() {
|
||
$('run').disabled = true;
|
||
$('run').textContent = 'Läuft...';
|
||
$('output').innerHTML = '';
|
||
$('summary').style.display = 'none';
|
||
|
||
const tests = [
|
||
{ id: 't-2p', label: '2 Spieler – Happy Path', fn: () => runHappyPath(2) },
|
||
{ id: 't-3p', label: '3 Spieler – Happy Path', fn: () => runHappyPath(3) },
|
||
{ id: 't-5p', label: '5 Spieler – Happy Path', fn: () => runHappyPath(5) },
|
||
{ id: 't-timeout', label: 'Timeout – einer startet nicht', fn: runTimeoutTest },
|
||
{ id: 't-cheat', label: 'Betrug – falsches Secret im Reveal', fn: runCheatTest },
|
||
{ id: 't-sel', label: 'Selective Abort – Cheater entscheidet nach Wunsch (10 Trials)', fn: () => runSelectiveAbort(10) },
|
||
];
|
||
|
||
for (const t of tests) addResult(t.id, t.label, 'running', '');
|
||
|
||
let passed = 0, failed = 0;
|
||
for (const t of tests) {
|
||
try {
|
||
const { pass, detail } = await t.fn();
|
||
updateResult(t.id, pass ? 'pass' : 'fail', detail);
|
||
if (pass) passed++; else failed++;
|
||
} catch (e) {
|
||
updateResult(t.id, 'fail', `Exception: ${e.message}`);
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
const summary = $('summary');
|
||
summary.style.display = 'block';
|
||
summary.textContent = `${passed}/${passed + failed} Tests bestanden`;
|
||
summary.className = 'summary ' + (failed === 0 ? 'pass' : 'fail');
|
||
$('run').disabled = false;
|
||
$('run').textContent = 'Tests ausführen';
|
||
}
|
||
|
||
$('run').addEventListener('click', runTests);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|