Update user and developer documentation for the protocol-8 system: persistent SPKI-derived identities, exact catalog ContentId authority, pinned responder pulls, structured runtime ownership, direct-author Call to Play, and the global local-network sharing switch. Remove active descriptions of repository-wide certificates, pushed deltas, relayed histories, and metadata consensus. Keep operational and UI boundaries aligned with the implementation, including the fail-closed production catalog gate. Test Plan: - `just fmt` (passed) - `git diff --cached --check` (passed)
lanspread-peer
lanspread-peer is the networking runtime that lets Lanspread nodes find each
other on the local network, exchange library metadata, and transfer game files.
It is designed to run headless – other crates (most notably
lanspread-tauri-deno-ts) embed it and drive it through a channel-based API.
Runtime Overview
start_peer(game_dir, tx_events, peer_game_db, unpacker, catalog)boots the asynchronous runtime in the background and returns aPeerRuntimeHandlewhose sender controls the peer. The injectedUnpackerkeeps archive extraction out of the peer crate's platform layer, and the immutableCatalogBundlegates which local game roots are announced or served.PeerCommandis the UI-facing control surface for complete library views, exact-content downloads and installs, game-directory changes, direct typed endpoint connections, and locally authored Call-to-Play intents.PeerEventenumerates everything the peer runtime reports back to the UI: wholesale remote-library and Call-to-Play views, download/install/uninstall lifecycle updates, runtime failures, and authenticated peer membership changes.PeerGameDBcollects metadata only for TLS-authenticated peer endpoints. It records the endpoint generation, runtime session, per-domain revisions, and exactGameAvailability { game_id, content_id }rows supplied by that pinned responder. It does not accept remote file manifests or descriptions.
Internally the peer runtime owns five coordinated services that run for the lifetime of the process:
- Server component (
run_server_component) – listens for QUIC connections, advertises via mDNS, and serves boundedPing,Hello, change-hint, exact-content chunk, and Stream Install requests. - Discovery loop (
run_peer_discovery) – uses thelanspread-mdnshelper to observe candidate endpoints. A dedicated joinable OS thread owns the blocking browser. Its nonblocking ingress queue and the async set of active candidate negotiations are each bounded at 64; repeated observations that overflow ingress are coalesced by dropping the hint. - Ping service (
run_ping_service) – periodically issues QUIC ping requests to keep peer liveness up to date and prunes stale entries fromPeerGameDB. Every probe captures the authenticated endpoint generation; success, failure, and stale removal are conditional on that exact endpoint and generation, so delayed work cannot affect a reauthenticated peer. - Local game monitor (
run_local_game_monitor) – polls a bounded non-recursive metadata snapshot of the configured game directory and its direct game roots once per second, gates per-ID rescans while operations are active, emits local-library changes separately from active-operation snapshots, and runs a 300-second fallback scan for broader reconciliation. - State-sync scheduler (
run_state_sync) – coalesces lossy revision hints, compares authenticatedPongrevisions, and performs boundedHellopulls from exact pinned responders. Its queue and peer table are each capped at 64 entries, with at most eight pulls and eight hint sends running concurrently.
scan_local_library maintains a lightweight on-disk index and produces both a
GameDB and protocol summaries. A game is downloaded only when its root-level
version.ini sentinel exists; local/ being a directory is the install signal.
Networking and File Transfer
- Transport is handled by
s2n-quicwith its rustls provider. Every outbound operation requires a typedPeerEndpoint { peer_id, addr }and TLS 1.3 pins the responder's complete DER SPKI to that expected ID before delegating the real CertificateVerify signature to rustls. ALPN islanspread/8; resumption, tickets, 0-RTT, and half-RTT data are disabled. - Protocol messages are strict, fallible JSON structures defined in
lanspread-proto::{Request, Response}. Protocol 8 is the only accepted wire shape. A control stream carries exactly one request frame followed by EOF and at most one response; frames are capped at 8 MiB and control I/O is bounded by ten-second deadlines. - File transfers stream raw bytes over dedicated bidirectional QUIC streams.
peer::send_game_file_chunkservices catalog-authorized ranges. The bounded server admits at most 64 unauthenticated handshakes, 64 established connection scopes, and 32 control-stream tasks per connection.
Each installation owns one Ed25519 key and one self-issued certificate. PeerId
is lowercase unpadded RFC 4648 base32 of BLAKE3 over the certificate's complete
canonical DER SPKI. The desktop app loads or creates the strict version-1
peer-identity-v1.json record in Tauri's app-data directory. Record loading
checks the exact version, algorithm, canonical base64, size bounds,
certificate/key match, derived SPKI ID, and exact SAN. Opens and publication do
not follow symlinks or reparse points, creation does not clobber an existing
winner, corrupt bytes are preserved in quarantine before replacement, and Unix
private material is mode 0600.
Read, permission, quarantine, or write failure returns a typed Ephemeral
persistence outcome and uses a fresh identity for that runtime. The peer CLI's
--identity-file selects an existing record strictly and never repairs,
quarantines, replaces, or generates that explicit path. Identity persistence
supports one normal app-data owner and has no cross-process lease; a concurrent
same-user writer can cause availability or continuity loss, but retained
no-follow handles contain traversal and publication never overwrites a
concurrent winner.
Responder-owned state and Call to Play
Hello is an empty pull request. A successful pinned responder returns only its
own PeerStateSnapshot: one runtime-session ID, a revisioned local library
slice, and a revisioned local Call-to-Play author slice. The receiver validates
the library and Call-to-Play domains independently and commits them only while
the authenticated endpoint generation is still current. A peer never relays
another peer's library or Call-to-Play state.
Local changes send cheap LibraryChanged or CallToPlayChanged revision hints.
Hints are untrusted and lossy: an unknown claimed peer is ignored, and a known
hint can only coalesce a full pull from that peer's pinned endpoint. Pong
carries the responder's runtime session and both revisions, so idle liveness
also heals a dropped hint.
Call-to-Play is stored as bounded per-author slices. The core, not the UI,
creates event/call nonces and timestamps. Wire events contain a typed CallId,
event ID, timestamp, and action; author identity comes from the pinned responder
and the display name belongs to the author snapshot. Whole snapshots replace a
newer revision from the same runtime session. Participant events may be retained
but remain hidden until the call creator's direct author slice is present; no
event push, acknowledgement, union merge, relay, or tombstone path exists.
Download Pipeline
When the UI asks to download a game:
- The UI submits only the game ID. The peer core loads that game's immutable
CatalogContentManifestand constructs one root-confinedValidatedDownloadManifestbefore any filesystem mutation. Remote file descriptions do not choose local paths, sizes, chunks, or hashes. - Source selection requires a peer to advertise the exact catalog
(game_id, content_id)pair. Every chunk request repeats thatContentId, a typed canonical catalog path, and the exact catalog range; a version string alone never makes a peer eligible. download_game_filesrecovers any earlier attempt, parks an oldversion.inias.version.ini.discarded, and durably journals the exact old and proposed downloader-owned file sets and their catalog content IDs before preparing non-sentinel files. It then emitsPeerEvent::DownloadGameFilesBeginand builds a per-peer plan (build_peer_plans) that round-robins catalog chunks across the eligible peers.- Each plan is executed in its own task (
download_from_peer). Chunk requests use per-chunk QUIC streams and write into pre-created files. Every chunk, includingversion.ini, must have the catalog length and BLAKE3 digest. One absolute ten-minute deadline covers opening, requesting, receiving, and the post-receive checks for each ordinary chunk. DownloadProgressTrackersamples byte counters, transfer speed, and the number of unique peers that are actively streaming chunks. The Tauri UI sees those values together through the regular download-progress event.- Integrity failures quarantine the runtime-local
(PeerId, content_id)pair; changing the peer's address does not clear it. Transport failures remain retryable and do not create durable trust state. A failed chunk is tried at most once against every distinct eligible peer identity, with no separate numeric retry cap. version.inichunks are buffered in memory. After transfer, paths owned by the previous successful download but absent from the new manifest are removed. Payload files and their directories are synced before the new sentinel is committed last via.version.ini.tmpfollowed by an atomic rename. A sentinel rename whose directory sync fails leaves ownership pending for recovery instead of being reported as a durable success.- Failure, cancellation, and startup recovery use the journal to remove only
exact downloader-owned files. Unknown user files,
local/, and install transaction state are preserved. A regularversion.inibeside a pending journal proves that the final rename landed; otherwise recovery aborts the incomplete payload. - After a successful sentinel commit,
PeerEvent::DownloadGameFilesFinishedis emitted and the peer auto-runs the install transaction.
The no-transfer local shortcut requires both a catalog-matching local sentinel and settled ownership whose committed content ID exactly matches the requested catalog manifest. A version string or pre-content-ID ownership record alone is never treated as verified local content.
Streamed Install Pipeline
Low-disk installs use PeerCommand::StreamInstallGame instead of the normal
archive download pipeline. The peer core loads the catalog's extracted-file
manifest, rejects games that do not have one, selects sources advertising the
exact catalog ContentId, and emits the regular download/install lifecycle
events while streaming archive-expanded bytes directly into an isolated
StreamedInstallTransaction. Every Stream Install control-frame path, including
archive names, is a CanonicalCatalogPath; malformed framing is distinct from
an explicit sender error.
The sender admits Stream Install only when its catalog authorizes the feature
and the direct regular root .eti set exactly matches the catalog archive set;
missing, extra, or unsupported archives are rejected before provider work
begins. The receiver independently requires the exact archive set and the
complete catalog path/kind/size set, hashes every regular extracted file with
BLAKE3 while writing, and rejects any missing, unknown, repeated, or mismatched
file. Repeated directory entries are harmless and may appear across archives,
but a regular extracted path may appear only once globally.
The sender-side StreamInstallProvider writes control and chunk frames through
a cancellable StreamInstallFrameSink. If the QUIC writer fails because the
receiver cancelled or disconnected, the sink wakes any producer blocked on the
bounded frame channel and lets the transfer guard drop normally.
Each failed peer attempt rolls back its staging directory before trying the next
validated peer. A transaction that created a previously missing game root
removes that root again when rollback leaves it empty. Once staging has been
fully verified, the account, language, and persona settings are rewritten in
that staging tree before it is promoted to local/. The one-shot settings
marker is written only after successful promotion; if that marker write fails,
first play safely retries the rewrite. Post-promote intent cleanup failures are
logged for startup recovery rather than reported as a failed install.
PeerCommand::CancelDownload cancels the tracked download token for an active
transfer. The transfer task remains responsible for clearing
active_operations, discarding partial payload files, and refreshing the
settled local snapshot, so the UI continues to treat active-operation snapshots
as the single source of truth for whether a download is still running.
Install Transactions
Install, update, uninstall, and install-side startup recovery live under
src/install/. Install-side operation intent is stored atomically under the
configured peer state directory, at games/<game_id>/install_intent.json. Game
roots still use Lanspread-owned .local.installing/ and .local.backup/
directories marked by .lanspread_owned. Startup recovery combines the recorded
intent with the observed filesystem state and only deletes reserved directories
when intent or marker ownership proves they belong to Lanspread.
Download provenance is stored separately under
games/<game_id>/download_ownership/v1-<root_digest>/record.json in the peer
state directory. Each namespace is derived from the canonical configured games
directory, while the record retains the full lossless root identity for
collision and binding checks. Switching roots therefore preserves independent
recovery and removal authority for the same game ID in each tree. Downloaded-
file removal is deliberately separate from uninstall: it refuses installed or
in-flight roots, journals an empty pending generation, and deletes only the
regular sentinel plus paths proven by the last committed ownership set. Unknown
files, directories, and the game root remain untouched.
Ownership schema 2 binds committed and pending file generations to their exact catalog content IDs. A former singleton ownership record is migrated once into its derived root namespace using copy-first durable publication; it is not kept as a second live format. Split or conflicting migration evidence fails closed.
Legacy launcher-owned files in game directories are migrated by a dedicated pre-start phase. Normal install, recovery, scan, and transfer paths use only the configured state directory for launcher-owned metadata.
Integration with lanspread-tauri-deno-ts
The Tauri application embeds this crate in
crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:
LanSpreadStateholds onto the peer control channel, the latest aggregatedGameDB, per-game operation state, the immutable catalog bundle, and the user-selected game directory.- The Tauri commands (
request_games,install_game,update_game,remove_downloaded_game, andupdate_game_directory) translate UI actions intoPeerCommands. Tauri loads and validates the packagedgame.dbplus companion manifests once during setup;update_game_directoryvalidates the filesystem path before storing it, starts the peer runtime on demand with that same bundle, and mirrors installed/uninstalled state into the separate UI-facing database. - A background task consumes
PeerEvents and fans them out to the front-end via Tauri publish/subscribe events (games-list-updated,game-download-*,game-install-*,game-uninstall-*,peer-*). The Tauri crate now only provides the unrar sidecar through the injectedUnpacker; rollback and cleanup live in the peer transaction code.
Security & Operational Notes
- Outbound TLS authenticates the exact responder endpoint. Inbound hints carry
no authority, state snapshots are accepted only from a pinned pull, and file
requests are admitted against the sender's local catalog and exact
ContentId. There is no address-only dial or repository-shared certificate. - mDNS is a link-local source of bounded candidates, not roster authority. A candidate reaches peer/UI/library state only after the pinned outbound handshake commits its still-current negotiation lease.
- Long-running blocking mDNS calls are isolated on an owned worker thread whose shutdown is explicitly joined.
- File writes are catalog-verified per chunk, and root-level
version.iniis buffered and written only after the rest of the download has succeeded.
Known Limitations
PeerGameDBcurrently models the latest metadata that other peers advertise. If the UI needs to surface titles that only exist locally, additional merging with the locally scannedGameDBwill be required.- The download planner uses a simple round-robin and does not yet take per-peer throughput into account when distributing initial work.
Refer to the source (particularly src/lib.rs) for the exact message shapes and
state machines.