[IMP] add crypto protocol test

This commit is contained in:
Niels Göttsch 2026-06-14 16:33:14 +02:00
parent d01d616dc2
commit 37986d0776
No known key found for this signature in database
GPG key ID: E325AC85B57E4370

271
test-protocol.html Normal file
View file

@ -0,0 +1,271 @@
<!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>