randomp2p/test-protocol.html

271 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>