diff --git a/Cargo.lock b/Cargo.lock index fe06723f..1ed4038c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -486,7 +486,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "c0mpute-api" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "c0mpute-proto", @@ -501,7 +501,7 @@ dependencies = [ [[package]] name = "c0mpute-bench" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "blake3", @@ -512,7 +512,7 @@ dependencies = [ [[package]] name = "c0mpute-cli" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "axum", @@ -546,7 +546,7 @@ dependencies = [ [[package]] name = "c0mpute-core" -version = "0.3.6" +version = "0.3.7" dependencies = [ "aes-gcm", "anyhow", @@ -568,6 +568,7 @@ dependencies = [ "c0mpute-verify", "directories", "flate2", + "futures", "hex", "hkdf", "hmac", @@ -596,7 +597,7 @@ dependencies = [ [[package]] name = "c0mpute-doctor" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "serde", @@ -608,7 +609,7 @@ dependencies = [ [[package]] name = "c0mpute-envelope" -version = "0.3.6" +version = "0.3.7" dependencies = [ "blake3", "bs58", @@ -621,7 +622,7 @@ dependencies = [ [[package]] name = "c0mpute-gateway" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "axum", @@ -643,7 +644,7 @@ dependencies = [ [[package]] name = "c0mpute-market" -version = "0.3.6" +version = "0.3.7" dependencies = [ "c0mpute-envelope", "serde", @@ -653,7 +654,7 @@ dependencies = [ [[package]] name = "c0mpute-net" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "async-trait", @@ -671,7 +672,7 @@ dependencies = [ [[package]] name = "c0mpute-placement" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "async-trait", @@ -692,7 +693,7 @@ dependencies = [ [[package]] name = "c0mpute-proto" -version = "0.3.6" +version = "0.3.7" dependencies = [ "blake3", "hex", @@ -704,7 +705,7 @@ dependencies = [ [[package]] name = "c0mpute-sealed" -version = "0.3.6" +version = "0.3.7" dependencies = [ "aes", "aes-gcm", @@ -726,7 +727,7 @@ dependencies = [ [[package]] name = "c0mpute-secure-chat" -version = "0.3.6" +version = "0.3.7" dependencies = [ "aes-gcm", "anyhow", @@ -749,7 +750,7 @@ dependencies = [ [[package]] name = "c0mpute-store" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "blake3", @@ -767,7 +768,7 @@ dependencies = [ [[package]] name = "c0mpute-transcode" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "c0mpute-proto", @@ -780,7 +781,7 @@ dependencies = [ [[package]] name = "c0mpute-update" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "hex", @@ -795,7 +796,7 @@ dependencies = [ [[package]] name = "c0mpute-verify" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "blake3", diff --git a/Cargo.toml b/Cargo.toml index a59cc73c..ed7ea4f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ ] [workspace.package] -version = "0.3.6" +version = "0.3.7" edition = "2024" license = "MIT" repository = "https://github.com/profullstack/c0mpute" diff --git a/apps/tui/package.json b/apps/tui/package.json index b7fe7a9c..8f3b4ff5 100644 --- a/apps/tui/package.json +++ b/apps/tui/package.json @@ -1,6 +1,6 @@ { "name": "@c0mpute/tui", - "version": "0.3.6", + "version": "0.3.7", "private": true, "type": "module", "bin": { diff --git a/apps/web/package.json b/apps/web/package.json index de9fb97c..7ae4b78d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "c0mpute", - "version": "0.3.6", + "version": "0.3.7", "private": true, "type": "module", "scripts": { diff --git a/apps/web/src/app/getting-started/page.tsx b/apps/web/src/app/getting-started/page.tsx index 8bdeda43..2456a525 100644 --- a/apps/web/src/app/getting-started/page.tsx +++ b/apps/web/src/app/getting-started/page.tsx @@ -92,8 +92,11 @@ $ c0mpute hosting ls # sites a gateway knows, with live copy counts`}--name to update it. Any gateway serves it at{" "} /s/<site_id>/, for example{" "} - https://gateway.c0mpute.com/s/<site_id>/. Limits: - 256 MiB and 10,000 files per site. + https://gateway.c0mpute.com/s/<site_id>/, with + byte ranges so video seeks. Limits: 64 GiB per free site, 4 TiB + with --disks auto. Sites over 1 GiB are striped over the + network in 64 MiB groups; run the same command again to resume an + interrupted publish.

point a Moshpit name at it (free to register):

diff --git a/apps/web/src/app/releases/latest.json/route.ts b/apps/web/src/app/releases/latest.json/route.ts index d02f75ef..fcf78e57 100644 --- a/apps/web/src/app/releases/latest.json/route.ts +++ b/apps/web/src/app/releases/latest.json/route.ts @@ -28,7 +28,7 @@ export const dynamic = "force-dynamic"; * this in sync with the workspace version in `Cargo.toml` on each release so a * freshly built CLI reports "already latest" rather than a phantom upgrade. */ -const FALLBACK_VERSION = "0.3.6"; +const FALLBACK_VERSION = "0.3.7"; const FALLBACK_MIN_REQUIRED = "0.0.1"; type Channel = "stable" | "beta" | "nightly"; diff --git a/docs/hosting.md b/docs/hosting.md index 997a4e1d..2e3e4c6e 100644 --- a/docs/hosting.md +++ b/docs/hosting.md @@ -9,11 +9,21 @@ c0mpute hosting publish ./public --name my-site # first version c0mpute hosting publish ./public --name my-site # same name again = new version c0mpute hosting publish ./public --name my-site --domain blog.example.org c0mpute hosting ls # sites a gateway knows, with copy counts +c0mpute hosting get movies/a.mp4 # download one file, shows MB/s +c0mpute hosting rm --name my-site # take it down (publishes an empty version) ``` `publish` signs the site with this machine's identity (`/buyer/identity.key`), -seeds it, and stays up until three hosting nodes hold copies (default wait 10 min). -Only the machine holding that key can publish new versions of the site. +seeds it, and stays up until three hosting nodes hold copies (default wait 10 min; for a +large site, 10 min without progress). Only the machine holding that key can publish new +versions of the site. + +The folder is scanned in place: nothing is copied, and the publisher serves chunks +straight from the files while the network takes its copies, so publishing 50 GiB of +video does not need another 50 GiB of local disk. Hashes of unchanged files are kept in +`/hosting/scan-cache.json`, so running the same command again is quick. That +is also how to resume: if `publish` stops (Ctrl-C, laptop asleep), run it again. The +record already on the network is reused and holders keep every group they have. The site is then served by every gateway at `http://:7777/s//`. Paths resolve like a static host: `/` and directories map to `index.html`, an @@ -30,18 +40,113 @@ is used for misses. | Healing | A site with fewer than 3 live copies: each hosting node ranks the candidates (rendezvous hashing on site id + peer id). The top-ranked ones fetch a copy. A node that stops announcing stops counting after ~200 s and another node takes over. Above 5 copies the lowest-ranked holder lets go. A publisher seeding its own site is a source, not a copy. | | Serving | Any gateway: local chunks when it holds them, otherwise fetched over libp2p from the fastest live holder (measured round-trip, round-robin among equally fast ones), failing over to the next and then to the DHT. Every chunk is checked against its hash. | -Limits per site: 1 GiB, 10,000,000 files. Up to 5,000 files the manifest is one chunk; -above that it is a root listing parts of 4,000 files each (by first path), so a -gateway fetches only the part a request needs. Nodes before v0.3.0 cannot read -parted manifests and skip those sites. A node holds at most `hosting.max_bytes` -(default 5 GiB) of other people's sites, and at most 1 GiB per owner key. +## Limits + +| | | +|---|---| +| Site, paid (`--disks`, a v2 record) | 4 TiB | +| Site, free (v1 record) | 64 GiB | +| Files per site | 10,000,000 | +| One file | 128 GiB | +| Small sites other features publish (ad creatives, app images, podcast shows, git repos) | 1 GiB, built in memory | +| Node budget | `[hosting] max_bytes`; `0` (default) = what it holds plus 25% of the free space above the free-disk floor | +| Free-disk floor | a copy must leave max(10 GiB, 5% of the disk), capped at 50 GiB, free | +| Per owner, per node | 16 GiB of free sites; sites the node is a preferred (paid) host for do not count | ```toml # ~/.config/c0mpute/config.toml [hosting] -max_bytes = 5368709120 +max_bytes = 0 # 0 = follow the disk; or a fixed number of bytes ``` +The budget advertised to d1sks (`max_gib`) is the same number, so `--disks auto` only +picks disks with room for their share of the site. + +Errors say which limit was hit and what to do (`--disks auto` for a free site over +64 GiB, split the folder above 4 TiB). A hosting node logs the limit that made it +refuse a copy (`over this node's hosting budget`, `disk nearly full`, `per-owner limit`). + +## Large sites: striped placement + +A site up to 1 GiB is replicated whole, exactly as before. Above that it is +**striped**: its chunks are bucketed by hash into groups, one group per 64 MiB of the +site up to 8,192 groups (512 GiB); a bigger site keeps 8,192 groups and makes each one +bigger (512 MiB at the 4 TiB cap), so a group bitmap on the gossip stays at most 1 KiB. +The count comes from the size in the signed record, and every group is placed on its own: + +* The hosting nodes that rank highest for `(site, group)` (rendezvous hashing, the same + answer on every node) take that group until it has three copies. No node needs the + whole site; a 50 GiB folder spreads over the whole network. +* Hosting nodes gossip which groups they hold (a bitmap per site) and how many bytes + they can still take (`room`). Only nodes that announce `room` are counted on for a + group, and only when they have room for it. A group that stays short for 3 minutes + brings in the next-ranked node, so a chosen node that cannot take it never stalls it. +* Healing, copy counting, garbage collection, proof-of-storage challenges (a chunk of a + group the holder says it holds), budgets and the free-disk floor all work per group. + A node holds each group as soon as it is complete, so a restart resumes group by group. +* Paid sites: preferred hosts take every group when there are at most three of them (a + paid host holds the whole site), else the three that rank highest per group; each is + paid for its share (`size x 3 / preferred`). A multi-TiB site therefore needs disks + with that much room each; `--copies 5` cuts each host's share to 60% of the site. +* `c0mpute hosting ls` and `/hosting/v1/sites` show, for a striped site, `copies` = the + fewest copies of any group, plus `groups`, `short_groups` and `missing_groups`. + +Manifests: up to 5,000 files and 1 GiB the manifest is one chunk, byte for byte what +older nodes read. Above either, it is a root listing parts (by first path) of at most +4,000 files and 65,536 chunk hashes each, so every part is one chunk fetch even for a +4 TiB site; a gateway fetches only the part a request needs. Every holder of any group +keeps the root and all parts. + +Groups are defined over chunk hashes only (the group of a chunk is its hash modulo the +group count), never file contents. A gateway finds the holders of any chunk from the +record alone, and the same layout works for a site whose chunks are ciphertext. + +**Older nodes** refuse records over their own cap (256 MiB before v0.3.0, 1 GiB in +v0.3.0 to v0.3.2), so they never see a striped site: they neither hold nor serve it (a +gateway that old answers 404 for it), and they are never counted on for a group because +they do not announce `room`. Sites up to 1 GiB are unchanged and those nodes keep holding +and serving them. + +## Streaming and Range requests + +Gateways answer `Range` requests on `/s//...` and on custom domains: `206 Partial +Content` with `Content-Range`, only the chunks the range covers are fetched, and +`Accept-Ranges: bytes` is on every file, so `