135 lines
6.7 KiB
Markdown
135 lines
6.7 KiB
Markdown
# OpenSpec Change: drive-mesh-port
|
|
|
|
- **Change ID**: drive-mesh-port
|
|
- **Package**: `@suaveplan/drive-mesh` (new)
|
|
- **Status**: PROPOSED
|
|
- **Date**: 2026-04-28
|
|
|
|
## Summary
|
|
|
|
P2P mesh layer over the chunks plugin. `DriveMesh` orchestrates peer discovery, availability gossip (compressed bitsets per file), parallel chunk pull (multi-peer racing window), peer scoring, server fallback, and upload backpressure. Mesh consumes the `ChunkSource` interface returned by `@suaveplan/drive-plugin-chunks` — it does NOT import drive core. Mesh consumes one or more `DriveTransport` implementations (HTTPS, WebRTC, libp2p) for the wire.
|
|
|
|
## Why mesh is a separate package
|
|
|
|
A vault running on a server doesn't need mesh — it just wants `drive` + `storage-s3`. A browser session in the CDN scenario needs `drive` + chunks plugin + mesh + `drive-transport-webrtc` + `drive-transport-https` (server fallback). Bundling mesh into drive or chunks would push WebRTC peer code and Bloom-filter logic into every consumer.
|
|
|
|
## Trust model
|
|
|
|
- **Server is the root of trust.** Server signs every chunk and every manifest with its ed25519 key.
|
|
- **Peers are caches, never authorities.** Mesh treats every byte from a peer as untrusted until the chunks plugin's sig-verify passes.
|
|
- **Bad-sig peers are punished.** A peer that ever delivers a chunk with an invalid server signature is added to a deny list (configurable: per-session or persistent).
|
|
- Mesh never holds the file decryption key — that's in the manifest, decrypted client-side via the recipient's key.
|
|
|
|
## API surface
|
|
|
|
```ts
|
|
const mesh = new DriveMesh({
|
|
chunks, // ChunkSource handle from chunksPlugin
|
|
transports: [webrtcTransport, httpsTransport],
|
|
serverPubKey: Uint8Array,
|
|
serverFallback: { url: string, transport: "https" },
|
|
scheduler: { concurrency: 8, perChunkTimeoutMs: 4000, racingWindowMs: 200 },
|
|
peerPolicy: { maxConcurrentUploadsPerPeer: 4, blocklistOnBadSig: true },
|
|
rng: types.Rng,
|
|
clock: types.Clock,
|
|
});
|
|
|
|
await mesh.start(); // open transports, begin advertising
|
|
await mesh.fetchFile(fileId); // resolves manifest, schedules missing chunks
|
|
await mesh.fetchChunks(fileId, [12, 13, 14]);
|
|
mesh.on("chunk:received", ({ fileId, chunkId, fromPeer }) => …);
|
|
await mesh.stop();
|
|
```
|
|
|
|
## Sub-systems
|
|
|
|
### 1. Peer index (`PeerIndex`)
|
|
|
|
Tracks live peers and their advertised availability per file (compressed bitset keyed by `fileId`), latency EMAs, error rates. **This is an index overlay, not a registry.** Peer identity and lifecycle events come from `network-plugin-registry` via the `NetworkBus`. The index only adds file-specific metadata on top.
|
|
|
|
```ts
|
|
// drive-mesh consumes registry events from the network layer
|
|
bus.on("peer:joined", ({ peerId, tier, latency }) => {
|
|
driveMesh.peerIndex.add(peerId, { tier, latency });
|
|
});
|
|
bus.on("peer:left", ({ peerId }) => {
|
|
driveMesh.peerIndex.remove(peerId);
|
|
});
|
|
```
|
|
|
|
Methods: `addPeer(peerId, transport, metadata)`, `removePeer`, `peersFor(fileId, chunkId)`.
|
|
|
|
### 2. Availability gossip (`AvailabilityIndex`)
|
|
|
|
- Each peer maintains a `BitSet` per known fileId of chunks it has.
|
|
- On chunk acquisition, peer pushes `advertise(fileId, BitSet)` to known peers via the transport.
|
|
- On peer-join, peer pulls `advertisement(fileId)` from new neighbours.
|
|
- Bitsets are gzipped on the wire.
|
|
|
|
### 3. Parallel-pull scheduler (`Scheduler`)
|
|
|
|
- Takes a list of `(fileId, chunkId)` requests.
|
|
- For each, picks top-K peers by score from `peersFor(fileId, chunkId)`.
|
|
- Issues request to the best peer; after `racingWindowMs`, issues a parallel request to the second-best (cancelled when the first arrives).
|
|
- Per-chunk timeout `perChunkTimeoutMs` triggers retry on next-best peer.
|
|
- Honours `concurrency` global cap.
|
|
- After all peer attempts exhaust, falls back to `serverFallback.url` via the HTTPS transport.
|
|
|
|
### 4. Peer scoring (`PeerScore`)
|
|
|
|
- `score = w_latency / latency_ema + w_success * success_rate - w_errors * error_rate`.
|
|
- Updated on every chunk-receive completion or failure.
|
|
- Sig-verify failure → permanent score = -∞ (effectively blocked).
|
|
|
|
### 5. Upload backpressure (`UploadGovernor`)
|
|
|
|
- Limits concurrent uploads per peer (`maxConcurrentUploadsPerPeer`).
|
|
- Honours WebRTC datachannel `bufferedAmount` watermark; pauses sends when high.
|
|
- Emits `upload:throttled` events for observability.
|
|
|
|
### 6. NetworkBus integration
|
|
|
|
`DriveMesh` listens to `network-plugin-registry` events on the `NetworkBus` to maintain its `PeerIndex` without duplicating peer lifecycle tracking:
|
|
|
|
```ts
|
|
bus.on("peer:joined", ({ peerId, tier, latency }) => {
|
|
driveMesh.peerIndex.add(peerId, { tier, latency });
|
|
});
|
|
bus.on("peer:left", ({ peerId }) => {
|
|
driveMesh.peerIndex.remove(peerId);
|
|
});
|
|
bus.on("peer:updated", ({ peerId, latency }) => {
|
|
driveMesh.peerIndex.updateLatency(peerId, latency);
|
|
});
|
|
```
|
|
|
|
This ensures that `drive-mesh` and `network-plugin-registry` share a single source of truth for peer identity. File-specific availability bitsets remain the responsibility of `drive-mesh`'s `AvailabilityIndex`.
|
|
|
|
### 7. Manifest bootstrap
|
|
|
|
Defaults to **manifest-as-chunk** (chunks plugin's default). Mesh requests `manifest:<fileId>` from peers like any other chunk; if unavailable, falls back to `serverFallback.url`. `mesh.fetchFile(fileId)` is an end-to-end happy-path that:
|
|
|
|
1. Fetch manifest (peer or server).
|
|
2. Verify manifest sig.
|
|
3. Diff manifest's chunk list against `chunks.hasChunks(fileId)`.
|
|
4. Schedule missing chunks via Scheduler.
|
|
5. Resolve when bitset is full.
|
|
|
|
## Error codes
|
|
|
|
`MESH_CODES = defineCodes("MESH", [PEER_NOT_FOUND, NO_PEERS_AVAILABLE, ALL_PEERS_FAILED, SERVER_FALLBACK_FAILED, MANIFEST_FETCH_FAILED, ABORTED, TRANSPORT_ERROR, PEER_BLOCKED] as const)`. `MeshError extends SuaveplanError<MeshErrorCode>`.
|
|
|
|
## Dependencies
|
|
|
|
- **Consumes:** `@suaveplan/drive-plugin-chunks` (interface only — `ChunkSource`), `@suaveplan/drive-transport-https` (server fallback), and any other registered `DriveTransport`. Plus `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/streams`, `@opentelemetry/api`, `zod`.
|
|
- **Consumes (NetworkBus events):** `@suaveplan/network-plugin-registry` (`peer:joined`, `peer:left`, `peer:updated`). Drive-mesh does NOT import the registry package directly; it listens to bus events.
|
|
- **Does NOT depend on:** `@suaveplan/drive` core.
|
|
- **Blocked on:** `drive-plugin-chunks-port`, at least one `drive-transport-*-port` (HTTPS minimum for fallback), `network-plugin-registry` (for peer identity events).
|
|
|
|
## Out of scope
|
|
|
|
- Wire-protocol implementations → `drive-transport-https-port`, `drive-transport-webrtc-port`.
|
|
- DHT-based peer discovery → future change `drive-mesh-discovery-dht`.
|
|
- BitSwap-style pairwise want-list reconciliation → optimisation in v2.
|
|
- Sharing primitives (envelope encryption, grant tokens) → `@suaveplan/drive-sharing` (separate change).
|