157 lines
5.2 KiB
TypeScript
157 lines
5.2 KiB
TypeScript
|
|
#!/usr/bin/env bun
|
||
|
|
/**
|
||
|
|
* Shade 4.0 GA — combined soak harness.
|
||
|
|
*
|
||
|
|
* Runs the ratchet stack under sustained ping-pong load for a configurable
|
||
|
|
* duration. Designed to be wrapped in `systemd-run` / `nohup` / a Gitea
|
||
|
|
* scheduled job and left running for ≥ 2 weeks before tagging GA-stable.
|
||
|
|
*
|
||
|
|
* Usage:
|
||
|
|
* bun run scripts/soak.ts # default 1h
|
||
|
|
* bun run scripts/soak.ts --hours 168 # 1 week
|
||
|
|
* bun run scripts/soak.ts --hours 336 # 2 weeks (V4.0 §Soak)
|
||
|
|
* bun run scripts/soak.ts --hours 0.05 # 3 minute smoke (CI gate)
|
||
|
|
*/
|
||
|
|
import { ShadeSessionManager } from '../packages/shade-core/src/index.js';
|
||
|
|
import { SubtleCryptoProvider, MemoryStorage } from '../packages/shade-crypto-web/src/index.js';
|
||
|
|
|
||
|
|
interface Args {
|
||
|
|
hours: number;
|
||
|
|
pairs: number;
|
||
|
|
reportSeconds: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseArgs(argv: string[]): Args {
|
||
|
|
const out: Args = { hours: 1, pairs: 4, reportSeconds: 60 };
|
||
|
|
for (let i = 0; i < argv.length; i++) {
|
||
|
|
const a = argv[i];
|
||
|
|
if (a === '--hours') out.hours = parseFloat(argv[++i]!);
|
||
|
|
else if (a === '--pairs') out.pairs = parseInt(argv[++i]!, 10);
|
||
|
|
else if (a === '--report-seconds') out.reportSeconds = parseInt(argv[++i]!, 10);
|
||
|
|
else if (a === '--help') {
|
||
|
|
console.log('Usage: bun run scripts/soak.ts [--hours N] [--pairs N] [--report-seconds N]');
|
||
|
|
process.exit(0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Counters {
|
||
|
|
ratchetSent: number;
|
||
|
|
ratchetRecv: number;
|
||
|
|
ratchetErrs: number;
|
||
|
|
errors: { workload: string; message: string; ts: number }[];
|
||
|
|
}
|
||
|
|
|
||
|
|
function newCounters(): Counters {
|
||
|
|
return {
|
||
|
|
ratchetSent: 0,
|
||
|
|
ratchetRecv: 0,
|
||
|
|
ratchetErrs: 0,
|
||
|
|
errors: [],
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Pair {
|
||
|
|
id: number;
|
||
|
|
aliceMgr: ShadeSessionManager;
|
||
|
|
bobMgr: ShadeSessionManager;
|
||
|
|
aliceAddr: string;
|
||
|
|
bobAddr: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function setupPair(id: number): Promise<Pair> {
|
||
|
|
const crypto = new SubtleCryptoProvider();
|
||
|
|
const aliceMgr = new ShadeSessionManager(crypto, new MemoryStorage());
|
||
|
|
const bobMgr = new ShadeSessionManager(crypto, new MemoryStorage());
|
||
|
|
await aliceMgr.initialize();
|
||
|
|
await bobMgr.initialize();
|
||
|
|
|
||
|
|
const otpks = await bobMgr.generateOneTimePreKeys(5);
|
||
|
|
const bundle = await bobMgr.createPreKeyBundle();
|
||
|
|
bundle.oneTimePreKey = { keyId: otpks[0]!.keyId, publicKey: otpks[0]!.keyPair.publicKey };
|
||
|
|
|
||
|
|
const aliceAddr = `alice-${id}`;
|
||
|
|
const bobAddr = `bob-${id}`;
|
||
|
|
await aliceMgr.initSessionFromBundle(bobAddr, bundle);
|
||
|
|
|
||
|
|
// Establish the receiver session by sending a first message and decrypting.
|
||
|
|
const init = await aliceMgr.encrypt(bobAddr, 'init');
|
||
|
|
await bobMgr.decrypt(aliceAddr, init);
|
||
|
|
|
||
|
|
return { id, aliceMgr, bobMgr, aliceAddr, bobAddr };
|
||
|
|
}
|
||
|
|
|
||
|
|
async function ratchetPingPong(pair: Pair, counters: Counters, abort: AbortSignal) {
|
||
|
|
const { aliceMgr, bobMgr, aliceAddr, bobAddr } = pair;
|
||
|
|
let i = 0;
|
||
|
|
while (!abort.aborted) {
|
||
|
|
try {
|
||
|
|
const env = await aliceMgr.encrypt(bobAddr, `msg-${i++}`);
|
||
|
|
counters.ratchetSent++;
|
||
|
|
await bobMgr.decrypt(aliceAddr, env);
|
||
|
|
counters.ratchetRecv++;
|
||
|
|
const reply = await bobMgr.encrypt(aliceAddr, `re-${i}`);
|
||
|
|
counters.ratchetSent++;
|
||
|
|
await aliceMgr.decrypt(bobAddr, reply);
|
||
|
|
counters.ratchetRecv++;
|
||
|
|
if ((i & 0xff) === 0) await new Promise((r) => setTimeout(r, 0));
|
||
|
|
} catch (err) {
|
||
|
|
counters.ratchetErrs++;
|
||
|
|
counters.errors.push({
|
||
|
|
workload: `pair-${pair.id}`,
|
||
|
|
message: (err as Error).message,
|
||
|
|
ts: Date.now(),
|
||
|
|
});
|
||
|
|
if (counters.ratchetErrs > 8) break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
const args = parseArgs(process.argv.slice(2));
|
||
|
|
const totalMs = Math.max(60_000, Math.round(args.hours * 3_600_000));
|
||
|
|
const ac = new AbortController();
|
||
|
|
const counters = newCounters();
|
||
|
|
const start = Date.now();
|
||
|
|
|
||
|
|
console.log(`[soak] start — ${args.pairs} pairs, ${args.hours}h, report every ${args.reportSeconds}s`);
|
||
|
|
|
||
|
|
const pairs = await Promise.all(Array.from({ length: args.pairs }, (_, i) => setupPair(i)));
|
||
|
|
|
||
|
|
setTimeout(() => ac.abort(), totalMs).unref();
|
||
|
|
process.on('SIGINT', () => ac.abort());
|
||
|
|
process.on('SIGTERM', () => ac.abort());
|
||
|
|
|
||
|
|
const reporter = setInterval(() => {
|
||
|
|
const elapsedS = ((Date.now() - start) / 1000).toFixed(0);
|
||
|
|
const rps = (counters.ratchetSent / Math.max(1, (Date.now() - start) / 1000)).toFixed(1);
|
||
|
|
console.log(
|
||
|
|
`[soak] t=${elapsedS}s sent=${counters.ratchetSent} recv=${counters.ratchetRecv} errs=${counters.ratchetErrs} (${rps} sent/s)`,
|
||
|
|
);
|
||
|
|
if (counters.errors.length > 32) counters.errors.splice(0, counters.errors.length - 32);
|
||
|
|
}, args.reportSeconds * 1000);
|
||
|
|
reporter.unref();
|
||
|
|
|
||
|
|
await Promise.all(pairs.map((p) => ratchetPingPong(p, counters, ac.signal)));
|
||
|
|
|
||
|
|
clearInterval(reporter);
|
||
|
|
|
||
|
|
const elapsedS = ((Date.now() - start) / 1000).toFixed(0);
|
||
|
|
console.log(`\n[soak] done in ${elapsedS}s`);
|
||
|
|
console.log(` ratchet sent ${counters.ratchetSent}`);
|
||
|
|
console.log(` ratchet recv ${counters.ratchetRecv}`);
|
||
|
|
console.log(` ratchet errs ${counters.ratchetErrs}`);
|
||
|
|
|
||
|
|
if (counters.ratchetErrs > 0) {
|
||
|
|
console.error('\nFAIL: ratchet errors recorded:');
|
||
|
|
for (const e of counters.errors.slice(-10)) {
|
||
|
|
console.error(` [${new Date(e.ts).toISOString()}] ${e.workload}: ${e.message}`);
|
||
|
|
}
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
process.exit(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
void main();
|