first commit
This commit is contained in:
53
docs/networking/drive/drive-mesh-protocol/src/trust/decay.md
Normal file
53
docs/networking/drive/drive-mesh-protocol/src/trust/decay.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# decay
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements the probabilistic-skip decay logic (DMP-15) the request scheduler uses to decide whether to include a peer in the candidate set on each new chunk request. When a peer's miss rate (`missing / advertised`) exceeds the floor (`0.05`), the probability of skipping the peer on each subsequent request is `(missRate - 0.05) * 20`, capped at `1.0`. The decay encodes the empirical observation that peers who advertise chunks they don't actually have should be progressively de-prioritised without being permanently blocked.
|
||||
|
||||
## Features
|
||||
|
||||
- `probabilityOfSkip(entry)` — pure function over `TrustEntry`; returns `[0, 1]`.
|
||||
- `shouldSkip(entry, rng)` — deterministic decision given an injected `Rng`.
|
||||
- Honours terminated entries (`probability === 1`) so the request scheduler never forgets a bad-sig event.
|
||||
- Honours zero-advertised entries (`probability === 0`) so freshly-met peers are never punished.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { probabilityOfSkip, TrustMap } from "@suaveplan/drive-mesh-protocol";
|
||||
|
||||
const t = new TrustMap();
|
||||
t.recordAdvertised("peer", "f", 100);
|
||||
t.recordMissing("peer", "f", 30);
|
||||
const p = probabilityOfSkip(t.get("peer", "f"));
|
||||
console.log(p); // 1 (capped at 1.0; 0.30 - 0.05 = 0.25; 0.25 * 20 = 5)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
shouldSkip,
|
||||
TrustMap,
|
||||
type TrustEntry,
|
||||
} from "@suaveplan/drive-mesh-protocol";
|
||||
import { defaultRng } from "@suaveplan/bitpacker-network";
|
||||
|
||||
const trust = new TrustMap();
|
||||
const rng = defaultRng();
|
||||
|
||||
function selectCandidates(peers: string[], fileId: string): TrustEntry[] {
|
||||
return peers
|
||||
.map((peerId) => trust.get(peerId, fileId))
|
||||
.filter((entry) => !shouldSkip(entry, rng));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `probabilityOfSkip(entry: TrustEntry): number` — returns `1` for terminated entries, `0` for `advertised === 0` or `missRate ≤ 0.05`, otherwise `(missRate - 0.05) * 20` clamped to `[0, 1]`.
|
||||
- `shouldSkip(entry: TrustEntry, rng: Rng): boolean` — short-circuits when probability is `0` or `1` to avoid burning RNG draws.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `Math.max(0, p)` clamp is defence-in-depth — the early-return on `missRate <= floor` already guarantees `p > 0` past that point — but JIT optimisers handle it cheaply and it documents the intent. The slope (`20`) is chosen so a peer at 10% miss rate is skipped on 100% of requests; below 10% the skip probability falls off linearly toward the floor at 5%. Tuning these numbers requires re-pinning `TRUST_MISS_RATE_FLOOR` / `TRUST_DECAY_SLOPE` in the schemas module — never hard-code them at call sites.
|
||||
@@ -0,0 +1,68 @@
|
||||
# sample
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements the adaptive sample-verification rate (DMP-17). On every incoming `cd:advertise`, the protocol MAY request a single chunk from the advertised set to verify the peer actually has it. The default rate is 1-in-100 for healthy peers and 1-in-10 for "suspect" peers (trust score `< 0.95` but `> -Infinity`). Sample-verification failure increments the `missing` counter via DMP-15.
|
||||
|
||||
## Features
|
||||
|
||||
- `classify(entry)` — three-way bucket (`terminated | suspect | healthy`).
|
||||
- `sampleRate(entry)` — denominator (`Infinity | 10 | 100`) — `Infinity` for terminated peers.
|
||||
- `shouldSample(entry, rng)` — deterministic decision given an injected `Rng`.
|
||||
- Constants pinned in `schemas/envelope.schema.ts` so dashboards and the spec share a source of truth.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
classify,
|
||||
sampleRate,
|
||||
TrustMap,
|
||||
} from "@suaveplan/drive-mesh-protocol";
|
||||
|
||||
const t = new TrustMap();
|
||||
t.recordAdvertised("peer", "f", 100);
|
||||
t.recordMissing("peer", "f", 10);
|
||||
const e = t.get("peer", "f");
|
||||
console.log(classify(e)); // "suspect"
|
||||
console.log(sampleRate(e)); // 10
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
classify,
|
||||
shouldSample,
|
||||
TrustMap,
|
||||
} from "@suaveplan/drive-mesh-protocol";
|
||||
import { defaultRng } from "@suaveplan/bitpacker-network";
|
||||
|
||||
const trust = new TrustMap();
|
||||
const rng = defaultRng();
|
||||
|
||||
async function onIncomingAdvertise(
|
||||
peerId: string,
|
||||
fileId: string,
|
||||
advertisedIds: number[]
|
||||
): Promise<void> {
|
||||
const e = trust.get(peerId, fileId);
|
||||
if (classify(e) === "terminated") return;
|
||||
if (shouldSample(e, rng)) {
|
||||
const chunkId = advertisedIds[Math.floor(rng.nextFloat() * advertisedIds.length)];
|
||||
const ok = await verifyAdvertisedChunk(peerId, fileId, chunkId);
|
||||
if (!ok) trust.recordMissing(peerId, fileId, 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type TrustStatus = "terminated" | "suspect" | "healthy"`.
|
||||
- `classify(entry: TrustEntry): TrustStatus`.
|
||||
- `sampleRate(entry: TrustEntry): number` — denominator; `Infinity` for terminated.
|
||||
- `shouldSample(entry: TrustEntry, rng: Rng): boolean`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Terminated peers are never sampled because the request scheduler shouldn't be talking to them in the first place; the function returns `false` early without consuming an RNG draw. The `1 / denom` probability uses 32-bit float division which has more than enough precision for a 1-in-100 / 1-in-10 decision; switching to integer division would buy nothing and force callers to track the denominator separately. Adjusting the suspect threshold or the per-status rate requires re-pinning the constants in `schemas/envelope.schema.ts` — the spec file MUST move in lockstep.
|
||||
64
docs/networking/drive/drive-mesh-protocol/src/trust/score.md
Normal file
64
docs/networking/drive/drive-mesh-protocol/src/trust/score.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# score
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements `TrustMap`, the per-`(peerId, fileId)` trust state container the request scheduler in `drive-mesh` uses to decide which peers to talk to (DMP-13, DMP-14, DMP-16). Each entry tracks how many chunks a peer claimed to have via gossip vs. how many it failed to deliver on request, plus a terminal `terminated: true` flag set after a bad-signature event.
|
||||
|
||||
## Features
|
||||
|
||||
- `TrustMap` — mutable, in-process map of `TrustEntry` records keyed by `(peerId, fileId)`.
|
||||
- `recordAdvertised(peerId, fileId, delta)` — increments the advertised counter; recomputes the score.
|
||||
- `recordMissing(peerId, fileId, delta)` — increments the missing counter; recomputes the score.
|
||||
- `terminate(peerId, fileId)` — pins the score to `-Infinity` after a bad-sig event (DMP-14).
|
||||
- `canServe(peerId, fileId)` — query helper for the request scheduler.
|
||||
- `dropPeer(peerId)` / `dropFile(fileId)` — cleanup hooks for the registry's `peer:left` / file-removal events.
|
||||
- Iterable via `Symbol.iterator` for snapshot/persistence.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { TrustMap } from "@suaveplan/drive-mesh-protocol";
|
||||
|
||||
const trust = new TrustMap();
|
||||
trust.recordAdvertised("peer-A", "fileId", 100);
|
||||
trust.recordMissing("peer-A", "fileId", 5);
|
||||
console.log(trust.get("peer-A", "fileId").score); // 0.95
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
classify,
|
||||
sampleRate,
|
||||
shouldSkip,
|
||||
TrustMap,
|
||||
} from "@suaveplan/drive-mesh-protocol";
|
||||
|
||||
const trust = new TrustMap();
|
||||
|
||||
function pickPeers(candidates: string[], fileId: string, rng): string[] {
|
||||
return candidates.filter((peerId) => {
|
||||
const e = trust.get(peerId, fileId);
|
||||
if (!trust.canServe(peerId, fileId)) return false;
|
||||
return !shouldSkip(e, rng);
|
||||
});
|
||||
}
|
||||
|
||||
function onBadSignature(peerId: string, fileId: string): void {
|
||||
trust.terminate(peerId, fileId);
|
||||
}
|
||||
|
||||
function onPeerDisconnect(peerId: string): void {
|
||||
trust.dropPeer(peerId);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class TrustMap` — `get(peerId, fileId)`, `recordAdvertised(peerId, fileId, delta)`, `recordMissing(peerId, fileId, delta)`, `terminate(peerId, fileId)`, `canServe(peerId, fileId)`, `dropPeer(peerId)`, `dropFile(fileId)`, `size`, `[Symbol.iterator]()`.
|
||||
- `interface TrustEntry` — `{ peerId, fileId, advertised, missing, score, terminated }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`recordAdvertised` and `recordMissing` are no-ops on terminated entries to prevent a bad-sig peer from regaining trust by spamming further advertisements; the score stays at `-Infinity` regardless of subsequent counters. The score formula is `1 - missing/advertised` with `score = 1` when `advertised === 0` so a freshly-met peer starts trusted by default — terminated entries override this. The iterator yields `TrustEntry` records by reference; consumers must NOT mutate them outside the public methods.
|
||||
Reference in New Issue
Block a user