Compare commits
3 commits
021f185924
...
fc81c1108b
| Author | SHA1 | Date | |
|---|---|---|---|
| fc81c1108b | |||
| 37986d0776 | |||
|
|
d01d616dc2 |
2 changed files with 276 additions and 2 deletions
|
|
@ -57,9 +57,12 @@ jobs:
|
|||
args:
|
||||
- |
|
||||
set -e
|
||||
git clone https://git.ziemlichoptimal.de/niels/randomp2p.git /work/repo
|
||||
git init /work/repo
|
||||
cd /work/repo
|
||||
git checkout ${SHA}
|
||||
git remote add origin https://git.ziemlichoptimal.de/niels/randomp2p.git
|
||||
git fetch --depth 1 origin ${SHA}
|
||||
git checkout FETCH_HEAD
|
||||
sed -i "s/HASH/${SHA}/g" /work/repo/index.html
|
||||
rm -rf /work/repo/.git
|
||||
volumeMounts:
|
||||
- { name: work, mountPath: /work }
|
||||
|
|
|
|||
271
test-protocol.html
Normal file
271
test-protocol.html
Normal 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>
|
||||
Loading…
Reference in a new issue