/** * DMP-13 .. DMP-17 — Verification, trust decay, sample-verification rate. * * `TrustMap` tracks `(peerId, fileId)` advertised/missing counts; * `shouldSkip` implements the probabilistic decay; `shouldSample` picks * the sampling rate based on current trust score; `terminate` pins the * score to `-Infinity` after a bad-signature event (DMP-14) without * touching unrelated files (DMP-16). */ import { describe, expect, it } from "@suaveplan/testing/runner"; import type { Rng } from "@suaveplan/types"; import { classify, probabilityOfSkip, SAMPLE_RATE_HEALTHY, SAMPLE_RATE_SUSPECT, sampleRate, shouldSample, shouldSkip, TRUST_DECAY_SLOPE, TRUST_MISS_RATE_FLOOR, TRUST_SUSPECT_THRESHOLD, TrustMap, } from "../src/index.js"; const FILE = "F".repeat(64); const OTHER = "G".repeat(64); function rngWith(values: number[]): Rng { let i = 0; const next = (): number => { const v = values[i % values.length] as number; i++; return v; }; return { nextUint32: () => Math.floor(next() * 2 ** 32), nextFloat: next, bytes: (n: number) => new Uint8Array(n).map(() => Math.floor(next() * 256)), }; } describe("DMP-13 — verification ordering", () => { it("chunk with bad signature is discarded and not committed", { timeout: 5000, }, () => { const trust = new TrustMap(); trust.recordAdvertised("peer", FILE, 100); // Simulate the request scheduler discovering a bad signature: // it terminates trust, never commits the chunk. trust.terminate("peer", FILE); expect(trust.canServe("peer", FILE)).toBe(false); const entry = trust.get("peer", FILE); expect(entry.terminated).toBe(true); expect(entry.score).toBe(Number.NEGATIVE_INFINITY); }); }); describe("DMP-14 — bad-signature trust termination", () => { it("bad signature sets per-file trust to -Infinity", { timeout: 5000, }, () => { const t = new TrustMap(); t.terminate("peer", FILE); const e = t.get("peer", FILE); expect(e.score).toBe(Number.NEGATIVE_INFINITY); expect(probabilityOfSkip(e)).toBe(1); }); }); describe("DMP-15 — probabilistic skip on advertised-but-missing", () => { it("miss rate below floor keeps skip probability at zero", { timeout: 5000, }, () => { const t = new TrustMap(); t.recordAdvertised("peer", FILE, 100); t.recordMissing("peer", FILE, 4); const e = t.get("peer", FILE); expect(probabilityOfSkip(e)).toBe(0); }); it("peer at 30% miss rate skipped on 50% of requests", { timeout: 5000, }, () => { const t = new TrustMap(); t.recordAdvertised("peer", FILE, 100); t.recordMissing("peer", FILE, 30); const e = t.get("peer", FILE); // (0.30 - 0.05) * 20 = 5, capped to 1.0 expect(probabilityOfSkip(e)).toBe(1); // At 7.5% miss: (0.075 - 0.05) * 20 = 0.5 const t2 = new TrustMap(); t2.recordAdvertised("p2", FILE, 1000); t2.recordMissing("p2", FILE, 75); expect(probabilityOfSkip(t2.get("p2", FILE))).toBeCloseTo(0.5, 4); expect(TRUST_MISS_RATE_FLOOR).toBe(0.05); expect(TRUST_DECAY_SLOPE).toBe(20); }); it("shouldSkip uses the injected Rng", { timeout: 5000 }, () => { const t = new TrustMap(); t.recordAdvertised("peer", FILE, 100); t.recordMissing("peer", FILE, 30); const e = t.get("peer", FILE); // Probability is capped at 1, so any RNG value < 1 returns true. expect(shouldSkip(e, rngWith([0.99]))).toBe(true); }); it("shouldSkip returns false for healthy peers", { timeout: 5000 }, () => { const t = new TrustMap(); t.recordAdvertised("peer", FILE, 100); const e = t.get("peer", FILE); expect(shouldSkip(e, rngWith([0.5]))).toBe(false); }); }); describe("DMP-16 — trust score per (peerId, fileId)", () => { it("trust score is per-file; one bad file does not affect others", { timeout: 5000, }, () => { const t = new TrustMap(); t.terminate("peer", FILE); t.recordAdvertised("peer", OTHER, 50); expect(t.canServe("peer", FILE)).toBe(false); expect(t.canServe("peer", OTHER)).toBe(true); expect(t.get("peer", OTHER).score).toBe(1); }); it("dropPeer cleans up only that peer's entries", { timeout: 5000 }, () => { const t = new TrustMap(); t.recordAdvertised("a", FILE, 1); t.recordAdvertised("b", FILE, 1); t.dropPeer("a"); expect(t.size).toBe(1); }); it("dropFile cleans up that file across peers", { timeout: 5000 }, () => { const t = new TrustMap(); t.recordAdvertised("a", FILE, 1); t.recordAdvertised("a", OTHER, 1); t.dropFile(FILE); expect(t.size).toBe(1); }); it("counts can be enumerated", { timeout: 5000 }, () => { const t = new TrustMap(); t.recordAdvertised("a", FILE, 5); const entries = [...t]; expect(entries.length).toBe(1); const first = entries[0] as (typeof entries)[number]; expect(first.advertised).toBe(5); }); it("rejects negative deltas", { timeout: 5000 }, () => { const t = new TrustMap(); expect(() => t.recordAdvertised("a", FILE, -1)).toThrow(); expect(() => t.recordMissing("a", FILE, -2)).toThrow(); }); }); describe("DMP-17 — adaptive sample-verification rate", () => { it("suspect peer sampled at 1-in-10; healthy peer at 1-in-100", { timeout: 5000, }, () => { const t = new TrustMap(); t.recordAdvertised("good", FILE, 100); t.recordAdvertised("bad", FILE, 100); t.recordMissing("bad", FILE, 10); const good = t.get("good", FILE); const bad = t.get("bad", FILE); expect(classify(good)).toBe("healthy"); expect(classify(bad)).toBe("suspect"); expect(sampleRate(good)).toBe(SAMPLE_RATE_HEALTHY); expect(sampleRate(bad)).toBe(SAMPLE_RATE_SUSPECT); expect(TRUST_SUSPECT_THRESHOLD).toBe(0.95); }); it("terminated peer is never sampled", { timeout: 5000 }, () => { const t = new TrustMap(); t.terminate("peer", FILE); const entry = t.get("peer", FILE); expect(classify(entry)).toBe("terminated"); expect(sampleRate(entry)).toBe(Number.POSITIVE_INFINITY); expect(shouldSample(entry, rngWith([0]))).toBe(false); }); it("shouldSample respects the rng draw and rate denominator", { timeout: 5000, }, () => { const t = new TrustMap(); t.recordAdvertised("peer", FILE, 100); const entry = t.get("peer", FILE); // Healthy: 1 in 100. RNG = 0.005 < 0.01 → sample. expect(shouldSample(entry, rngWith([0.005]))).toBe(true); // RNG = 0.5 → no sample. expect(shouldSample(entry, rngWith([0.5]))).toBe(false); }); }); describe("DMP — terminated entries", () => { it("recordAdvertised / recordMissing are no-ops once terminated", { timeout: 5000, }, () => { const t = new TrustMap(); t.terminate("peer", FILE); t.recordAdvertised("peer", FILE, 50); t.recordMissing("peer", FILE, 10); const e = t.get("peer", FILE); expect(e.advertised).toBe(0); expect(e.missing).toBe(0); expect(e.score).toBe(Number.NEGATIVE_INFINITY); }); });