From a0f9480c2fb34c93b0407c8f332682c00d9941fe Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 9 Nov 2026 13:53:42 +0000 Subject: [PATCH] c0mpute edge: mirror c0mpute 0.3.7 site size limits in verified reads c0mpute #222 (v0.3.7) raised site.rs limits for media-size sites; the edge still refused anything over 1 GiB from an untrusted gateway. - MAX_SITE_BYTES 1 GiB -> 4 TiB (v2 record size, inline total, parted root) - v1 records capped at MAX_FREE_SITE_BYTES (64 GiB), as check_fields does - MAX_PARTS = ceil(MAX_FILES/PART_FILES) + 2*ceil((MAX_SITE_BYTES/FILE_CHUNK_BYTES)/PART_CHUNKS) + 2 = 2630 MAX_FILE_BYTES (128 GiB) is a publish-time check only, not a parse rule, so it is not mirrored. Co-Authored-By: Claude Opus 5.5 --- lib/c0mpute-verify.ts | 16 ++++++++--- tests/c0mpute-verify.test.ts | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/lib/c0mpute-verify.ts b/lib/c0mpute-verify.ts index c330b2d..f98d1e6 100644 --- a/lib/c0mpute-verify.ts +++ b/lib/c0mpute-verify.ts @@ -27,11 +27,19 @@ import { blake3 } from "../vendor/noble-hashes/blake3.js"; /** Files are cut into chunks of this size (site.rs FILE_CHUNK_BYTES). */ export const FILE_CHUNK_BYTES = 1024 * 1024; -/** Largest site (site.rs MAX_SITE_BYTES). */ -export const MAX_SITE_BYTES = 1024 * 1024 * 1024; +const GIB = 1024 * 1024 * 1024; +/** Largest site: a v2 record, an inline manifest, a parted root (site.rs MAX_SITE_BYTES, 4 TiB). */ +export const MAX_SITE_BYTES = 4 * 1024 * GIB; +/** Largest site a v1 (free) record may claim (site.rs MAX_FREE_SITE_BYTES). */ +export const MAX_FREE_SITE_BYTES = 64 * GIB; const MAX_FILES = 10_000_000; const MAX_INLINE_FILES = 10_000; const PART_FILES = 4_000; +/** Chunks per manifest part, besides PART_FILES (site.rs PART_CHUNKS). */ +const PART_CHUNKS = 65_536; +/** Most parts a v2 root may list (site.rs MAX_PARTS): 2500 + 2 * 64 + 2 = 2630. */ +export const MAX_PARTS = Math.ceil(MAX_FILES / PART_FILES) + + 2 * Math.ceil(MAX_SITE_BYTES / FILE_CHUNK_BYTES / PART_CHUNKS) + 2; const HASH_RE = /^[0-9a-f]{64}$/; const KEY_RE = /^[0-9a-f]{64}$/; @@ -124,7 +132,7 @@ export function verifyRecord(input: unknown, siteId: string): SiteRecord { if (!NAME_RE.test(r.name)) fail("bad site name"); if (!KEY_RE.test(r.owner)) fail("owner is not a hex ed25519 key"); if (!HASH_RE.test(r.manifest)) fail("manifest is not a hash"); - if (r.size > MAX_SITE_BYTES) fail("site is over the size cap"); + if (r.size > (r.v >= 2 ? MAX_SITE_BYTES : MAX_FREE_SITE_BYTES)) fail("site is over the size cap"); if (!SITE_RE.test(siteId) || r.site_id !== siteId) fail("record is for another site"); if (siteIdFor(r.owner, r.name) !== r.site_id) fail("site id does not match owner and name"); if (!ed25519Verify(r.owner, r.sig, signingBytes({ ...r, domains }))) fail("bad record signature"); @@ -196,7 +204,7 @@ export function verifyManifest(bytes: Uint8Array, record: SiteRecord): Manifest const parts = m.parts as PartRef[]; if (!isU64(m.files) || !isU64(m.bytes) || !Array.isArray(parts) || parts.length === 0) fail("manifest root malformed"); if ((m.files as number) > MAX_FILES || (m.bytes as number) > MAX_SITE_BYTES) fail("manifest root out of bounds"); - if (parts.length > Math.ceil(MAX_FILES / PART_FILES)) fail("manifest has too many parts"); + if (parts.length > MAX_PARTS) fail("manifest has too many parts"); let files = 0, total = 0; for (const p of parts) { if (!p || typeof p.first !== "string" || typeof p.hash !== "string" || !isU64(p.files) || !isU64(p.bytes) diff --git a/tests/c0mpute-verify.test.ts b/tests/c0mpute-verify.test.ts index 3aad040..6d1740a 100644 --- a/tests/c0mpute-verify.test.ts +++ b/tests/c0mpute-verify.test.ts @@ -15,6 +15,9 @@ import { join } from "node:path"; import { ChunkCache, FILE_CHUNK_BYTES, + MAX_FREE_SITE_BYTES, + MAX_PARTS, + MAX_SITE_BYTES, SeqGuard, blake3hex, candidates, @@ -245,6 +248,59 @@ test("v2 parted sites: parts verified by hash, a left-out part is refused, skipp assert.throws(() => verifyResolve(s.id, "/zz/foo", lie), /resolved|left out/); }); +test("size caps mirror site.rs (c0mpute 0.3.7): 4 TiB per site, 64 GiB on a v1 record, 2630 parts", () => { + const GIB = 1024 ** 3; + assert.equal(MAX_SITE_BYTES, 4 * 1024 * GIB); + assert.equal(MAX_FREE_SITE_BYTES, 64 * GIB); + assert.equal(MAX_PARTS, 2630); + + const key = newKey(); + const record = (v: 1 | 2, size: number) => { + const base = { site_id: siteIdFor(key.owner, "media"), name: "media", owner: key.owner, manifest: blake3hex("m"), seq: 1, size, domains: [] as string[] }; + return v === 2 + ? signRecord(key, { v: 2, ...base, preferred_hosts: ["12D3KooWFTsCjAVPKYnak1H8N7By5UpgeipVScmuAyGeRzzsLwSu"], price_per_gib_month: "0.004" }) + : signRecord(key, { v: 1, ...base }); + }; + const id = siteIdFor(key.owner, "media"); + assert.equal(verifyRecord(record(2, 3 * GIB), id).size, 3 * GIB); + assert.equal(verifyRecord(record(2, MAX_SITE_BYTES), id).size, MAX_SITE_BYTES); + assert.throws(() => verifyRecord(record(2, MAX_SITE_BYTES + 1), id), /size cap/); + assert.equal(verifyRecord(record(1, 2 * GIB), id).size, 2 * GIB); + assert.equal(verifyRecord(record(1, MAX_FREE_SITE_BYTES), id).size, MAX_FREE_SITE_BYTES); + assert.throws(() => verifyRecord(record(1, MAX_FREE_SITE_BYTES + 1), id), /size cap/); +}); + +test("a v2 site over 1 GiB verifies end to end, with more parts than files alone would need", () => { + const GIB = 1024 ** 3; + const key = newKey(); + const name = "media"; + const index = Buffer.from("