From 5f887887be3113fb7368726712ab45b6e4672bf1 Mon Sep 17 00:00:00 2001 From: Yigit Gurbulak Date: Fri, 9 Oct 2026 02:50:45 +0300 Subject: [PATCH 1/3] docs(agents): sync readme and agents.md with the codebase Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 23 +++++++++++++---------- README.md | 22 ++++++++++++---------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 38972b0..1d0c7ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,10 +13,11 @@ Next.js 16 (App Router) · React 19 · Tailwind CSS v4 · TypeScript · pnpm. No ## Commands ```bash -pnpm install # required — husky hooks and lint-staged invoke `pnpm exec` +pnpm install # required — husky hooks shell out to `pnpm exec` (lint-staged, commitlint) pnpm dev # dev server on :3000 pnpm build # static export → ./out (chains check:out + check:og-image) -pnpm start # serve a prior `next build` output (not the static export) +pnpm start # unusable here — `next start` errors under `output: "export"`; + # preview the export with `npx serve@latest out` pnpm test # vitest run — `lib/blog.test.ts` for a pure-module test, # `components/ui/MethodRow.test.ts` for a component one pnpm typecheck # tsc --noEmit (CI) — `next build` only checks what it bundles @@ -30,16 +31,15 @@ pnpm check:og-image # postbuild social-card guard over ./out (runs inside `pnp ## Layout - `app/` — App Router entry (`layout.tsx`, `page.tsx`, `globals.css`), the `blog/` and `blog/[slug]/` routes, the `legal/[doc]` route, the `robots.txt/route.ts` / `sitemap.xml/route.ts` / `sitemap-pages.xml/route.ts` / `llms.txt/route.ts` / `llms-full.txt/route.ts` handlers, and the file-convention assets (`favicon.ico`, `icon.svg`, `apple-icon.png`, `opengraph-image.{png,alt.txt}`). -- `components/site/` — page sections composed by `app/page.tsx` (Hero, Compare, Method, Faq, Contact, …) plus chrome (`Chrome`, `Footer`, `ScrollReveal`, …). Section components are named after their section `id` (e.g. `Contact.tsx` for `id="contact"`); `Hero` is the idiomatic exception for the top `id="intro"` section. -- `components/ui/` — low-level primitives (Frame, SectionHeader, Prose, Figure, …). -- `lib/` — shared helpers (`links.ts`, `blog.ts`, `faq.ts`, `legal.ts`, `jsonld.tsx`, `schema.ts`, …). `lib/schema.ts` holds the site-level JSON-LD nodes; the per-post `BlogPosting` node is `blogPostingNode` in `lib/blog.ts` because it derives from a post rather than from the site — `blogNode` nests it and `app/blog/[slug]/page.tsx` emits it top-level, so they cannot diverge under the same `@id`. +- `components/site/` — page sections composed by `app/page.tsx` (Hero, Compare, Method, Faq, Contact, …) plus chrome (`Chrome`, `Footer`, `MobileMenu`, `ScrollReveal`, …) and the live panels (`HeroTerminal`, `FleetStatus`). Section components are named after their section `id` (e.g. `Contact.tsx` for `id="contact"`); `Hero` is the idiomatic exception for the top `id="intro"` section. +- `components/ui/` — low-level primitives (Frame, SectionHeader, Prose, Figure, PanelFigure, …). `PanelFigure` exists so a tool panel's caption sits outside its `aria-hidden` panel; its test and `check-out.mjs` both hold that. +- `lib/` — shared helpers (`links.ts`, `blog.ts`, `faq.ts`, `legal.ts`, `jsonld.tsx`, `schema.ts`, `metadata.ts`, `scroll.ts`, …). `lib/schema.ts` holds the site-level JSON-LD nodes; the per-post `BlogPosting` node is `blogPostingNode` in `lib/blog.ts` because it derives from a post rather than from the site — `blogNode` nests it and `app/blog/[slug]/page.tsx` emits it top-level, so they cannot diverge under the same `@id`. - `lib/copy.ts` — the site's prose, as plain data. New or edited homepage copy goes here, not inline in a component: the comparison table maps over `COMPARE_ROWS` and `app/llms-full.txt/route.ts` serialises the same strings as markdown, so copy inlined in JSX silently drifts from the mirror. This is not hypothetical — the hero and method stat strips were inline `
` attributes restated by hand in the mirror, and had already drifted. Values as well as sentences: `HERO_FIGURES`, `STACK`, `PANEL_CAPTIONS`. Render prose through `highlightBrand` (`components/ui/brand.tsx`) wherever the sentence names the product. - `lib/stats.ts` — the live testnet figures behind the hero terminal and the Contact fleet panel. The page stays a static export: both panels render a loading state into the HTML and fill in after mount from `https://data.decdn.org/stats-421614.json`, which the `decdn/stats` indexer rewrites every five minutes (schema: `worker/src/stats.ts` there). Shape check, formatting and view models are pure and unit-tested; `lib/use-stats.ts` is the one-fetch-per-page client hook. The CSP in `public/_headers` allowlists the origin under `connect-src` — change both together. - `test-utils/` — test-only helpers (`react-tree.ts` walks the element tree a server component returns). Nothing under `app/` or `components/` imports from here. -- `scripts/` — `check-out.mjs`, the postbuild assertions `pnpm build` runs against `./out`. - `content/blog/` — MDX posts loaded by `lib/blog.ts` and rendered via `app/blog/[slug]/page.tsx`. - `content/legal/` — MDX for the legal pages (`privacy`, `terms`, `disclaimer`) loaded by `lib/legal.ts` and rendered via `app/legal/[doc]/page.tsx`. -- `scripts/` — build-time guards run against the export, not shipped with it (`check-out.mjs` for advertised URLs and dotted routes, `check-og-image.mjs` for social cards; both chained off `pnpm build`). Node built-ins only, no dependencies; `scripts/*.test.ts` spawns them against fixture exports. +- `scripts/` — build-time guards run against the export, not shipped with it (`check-out.mjs` for advertised URLs, dotted routes and the `PANEL_CAPTIONS` strings in the built homepage; `check-og-image.mjs` for social cards; both chained off `pnpm build`). Node built-ins only, no dependencies; `scripts/check-og-image.test.ts` spawns its script against fixture exports (`check-out.mjs` has no test of its own). - `docs/` — Mintlify source for `docs.decdn.org` (separate build pipeline, not part of the static export). - Path alias `@/*` → project root (e.g. `@/lib/links`, not `@/src/...`). @@ -47,10 +47,13 @@ pnpm check:og-image # postbuild social-card guard over ./out (runs inside `pnp - **Static export only.** `next.config.ts` has `output: "export"`. No SSR, ISR, middleware, or Image Optimization API. Route handlers (`app/robots.txt/route.ts`, `app/sitemap.xml/route.ts`, `app/sitemap-pages.xml/route.ts`, `app/llms.txt/route.ts`, `app/llms-full.txt/route.ts`) are allowed only when statically generated at build time (`dynamic = "force-static"`, GET-only). `robots.txt` is a hand-written route handler rather than the Next `app/robots.ts` metadata file so it can emit non-standard directives (`Content-Signal:` per contentsignals.org). A dotted route segment is what makes these land as real files (`out/llms.txt`) rather than `/index.html` despite `trailingSlash: true`. - **`trailingSlash: true`.** `next.config.ts` emits every route as `/index.html` and canonical/internal links should expect a trailing slash. Cloudflare Pages serves `out/` as-is. -- **Tailwind v4.** `globals.css` uses `@import "tailwindcss"` and `@theme inline { … }`. There is no `tailwind.config.*` — theme tokens live in CSS. Don't reach for v3 directives. +- **Tailwind v4.** `globals.css` uses `@import "tailwindcss"` and `@theme inline { … }`. There is no `tailwind.config.*` — theme tokens live in CSS. Don't reach for v3 directives. Shared classes go in `@layer components` (focus rings in `@layer base`) so a utility beside one can refine it. Use the tokens — `--ease-expo` / `--ease-soft` / `--ease-term`, `--font-sans` / `--font-mono`, `--panel-radius` — rather than literal curves, stacks and radii. +- **Fonts are self-hosted.** `app/layout.tsx` loads Geist and Geist Mono through `next/font/google`, which downloads them at build time, so the CSP's `font-src 'self'` suffices; a font CDN would need a CSP change in `public/_headers`. `--font-mono-system` is the fallback for glyphs the Geist Mono subset lacks (→ ✓ Σ). - **Conventional commits required.** `commitlint` runs in the `commit-msg` husky hook; non-conforming messages are rejected. - **`metadataBase` is live.** `lib/links.ts` `site` is the real origin and `INDEXABLE` is `true`. Anything anchored on this origin — OG and canonical (via `metadataBase`); JSON-LD, `app/sitemap.xml/route.ts`, `app/sitemap-pages.xml/route.ts`, `app/robots.txt/route.ts` (via `SITE_URL`) — ships to production. Adding a new non-blog page = append an entry to `app/sitemap-pages.xml/route.ts`; blog posts auto-derive from `content/blog/` and legal pages from the closed `LEGAL_SLUGS` list in `lib/legal.ts`. Flip `INDEXABLE` to mark pages noindex; `robots.txt` and the sitemap remain unchanged by design (see `lib/links.ts` for why). `app/llms.txt/route.ts` and `app/llms-full.txt/route.ts` ship to the same origin and derive their page entries from `listPosts()` / `LEGAL_SLUGS` / `lib/copy.ts`; both are listed in `app/sitemap-pages.xml/route.ts` and advertised via `alternates.types` in `app/layout.tsx` and a `Link:` header in `public/_headers`. Deriving entries is not by itself a guarantee that a URL exists — the sitemap's two llms entries are literals, and the litepaper, press kit and `docs.decdn.org` entries are not routes at all. `scripts/check-out.mjs` is what resolves every advertised same-origin URL against `out/` after a build; `scripts/check-og-image.mjs` does the same for every `og:image` / `twitter:image` in every built page, plus every `sitemap-pages.xml` ``. -- **`docs/` is a different product.** Mintlify Cloud builds it from `docs/docs.json` and serves it at `docs.decdn.org` — independent of `pnpm build`. The website code must not import from `docs/`; ESLint and the website CI workflow ignore it. Edits to MDX go through the `docs` workflow (Prettier + `markdownlint-cli2` + `mintlify broken-links`). -- **vitest only collects `**/\*.test.ts`.** A `.test.tsx`is silently skipped — the suite stays green and the file never runs. Component tests therefore contain no JSX: call the component as the plain function it is and walk the result with`test-utils/react-tree.ts`(or`React.createElement` for fixtures). +- **`docs/` is a different product.** Mintlify Cloud builds it from `docs/docs.json` and serves it at `docs.decdn.org` — independent of `pnpm build`. The website code must not import from `docs/`; ESLint and the website CI workflow ignore it, but the root vitest config still collects `docs/*.test.ts`. Edits to MDX go through the `docs` workflow (Prettier + `markdownlint-cli2` + `pnpm test` for `docs/canonical-host.test.ts` + `mintlify broken-links`). +- **vitest only collects `.test.ts` files.** The include glob in `vitest.config.ts` is `**/*.test.ts`, so a `.test.tsx` is silently skipped — the suite stays green and the file never runs. Component tests therefore contain no JSX: call the component as the plain function it is and walk the result with `test-utils/react-tree.ts` (or `React.createElement` for fixtures). A client component that calls hooks can't be invoked that way — export its pure render half and test that (`FleetPanel` in `FleetStatus.tsx`); what's left is covered only by `scripts/check-out.mjs` against the built HTML. - **CI typechecks separately.** `next build` only typechecks what it bundles, so test files are invisible to it — one had a type error on `main`. `pnpm typecheck` is a distinct CI step. - **CSS custom properties in `style` props.** Known `--var` names are declared in `types/css.d.ts` (module-augmenting React's `CSSProperties`) so call sites can write `style={{ "--reveal-delay": "120ms" }}` without a cast. Add new vars there before using them. +- **Social links have three consumers.** A new entry in `links` (`lib/links.ts`) also belongs in `sameAs` in `lib/schema.ts` (asserted by `lib/schema.test.ts`) and in the contact line of `app/llms-full.txt/route.ts`. +- **ESLint majors are held back.** `.github/dependabot.yml` ignores `eslint` semver-major updates: `eslint-config-next` pulls in an `eslint-plugin-react` that crashes on v10. Lift it when Next ships a compatible plugin. diff --git a/README.md b/README.md index aaa7288..dabf543 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,19 @@ This is the source for the deCDN marketing site and blog. ## Stack -Next.js 16 (App Router) · React 19 · Tailwind CSS v4 · TypeScript · pnpm. Static export, deployed to Cloudflare Pages. +Next.js 16 (App Router) · React 19 · Tailwind CSS v4 · TypeScript · pnpm · Node ≥24 (`.nvmrc`). Static export, deployed to Cloudflare Pages. ## Getting started ```bash -pnpm install # required (not npm/yarn) — husky hooks shell out to `pnpm exec` -pnpm dev # dev server on :3000 -pnpm build # static export → ./out -pnpm test # vitest run -pnpm typecheck # tsc --noEmit -pnpm lint # eslint (flat config) -pnpm format # prettier --write . +pnpm install # required (not npm/yarn) — husky hooks shell out to `pnpm exec` +pnpm dev # dev server on :3000 +pnpm build # static export → ./out, then the check:out + check:og-image guards +pnpm test # vitest run +pnpm typecheck # tsc --noEmit +pnpm lint # eslint (flat config) +pnpm format # prettier --write . +pnpm format:check # prettier --check . (CI) ``` ## Project layout @@ -34,9 +35,9 @@ pnpm format # prettier --write . - `app/` — App Router entry (`layout.tsx`, `page.tsx`, `globals.css`) plus the blog routes (`blog/`, `blog/[slug]/`), the `legal/[doc]` route, the sitemap/robots handlers, the `llms.txt` / `llms-full.txt` handlers, and file-convention metadata assets. - `components/site/` — page sections composed by `app/page.tsx` (Hero, Compare, Method, Faq, Contact, …) plus chrome (`Chrome`, `Footer`, `ScrollReveal`, …). - `components/ui/` — low-level primitives (Frame, SectionHeader, Prose, Figure, …). -- `lib/` — shared helpers (`links.ts`, `copy.ts`, `blog.ts`, `faq.ts`, `legal.ts`, `jsonld.tsx`, `schema.ts`, …). The site's prose _and_ its displayed values live in `lib/copy.ts` as plain data — the comparison table maps over it and the `llms-full.txt` mirror serialises it, so anything inlined in a component drifts from the mirror. +- `lib/` — shared helpers (`links.ts`, `copy.ts`, `stats.ts`, `blog.ts`, `faq.ts`, `legal.ts`, `jsonld.tsx`, `schema.ts`, …). The site's prose _and_ its displayed values live in `lib/copy.ts` as plain data — the comparison table maps over it and the `llms-full.txt` mirror serialises it, so anything inlined in a component drifts from the mirror. Prose that names the product renders through `highlightBrand` (`components/ui/brand.tsx`). - `test-utils/` — test-only helpers for walking the element tree a server component returns. -- `scripts/` — `check-out.mjs`, postbuild assertions run against `./out` by `pnpm build`. +- `scripts/` — postbuild guards run against `./out` by `pnpm build`: `check-out.mjs` (advertised URLs, dotted routes, panel captions) and `check-og-image.mjs` (social cards). - `content/blog/` — MDX posts loaded by `lib/blog.ts` and rendered by `app/blog/[slug]/page.tsx`. - `content/legal/` — MDX for the legal pages (`privacy`, `terms`, `disclaimer`), loaded by `lib/legal.ts` and rendered by `app/legal/[doc]/page.tsx`. - `docs/` — Mintlify source for `docs.decdn.org`. Built and deployed independently of `pnpm build`; not part of the static export and not imported from the website code. @@ -50,6 +51,7 @@ pnpm format # prettier --write . - **Conventional commits enforced.** `commitlint` runs in the `commit-msg` husky hook; non-conforming messages are rejected. - **`metadataBase` is live.** `lib/links.ts` `site` is the real origin and `INDEXABLE` is `true`. Anything anchored on this origin — OG and canonical (via `metadataBase`); JSON-LD and the sitemap/robots emitters (via `SITE_URL`) — ships to production. Adding a non-blog page means appending an entry to `app/sitemap-pages.xml/route.ts`; blog posts are auto-derived from `content/blog/`, while legal pages are driven by the closed `LEGAL_SLUGS` list in `lib/legal.ts` (add both the MDX file under `content/legal/` and a slug there). Flipping `INDEXABLE` flips `` only; `robots.txt` and the sitemap stay unconditional by design (rationale in `lib/links.ts`). - **Machine-readable surfaces.** `/llms.txt` is a curated index of this origin and `/llms-full.txt` is every page as one plain-text document; both derive their page entries from `listPosts()`, `LEGAL_SLUGS`, `lib/faq.ts` and `lib/copy.ts`, are listed in `app/sitemap-pages.xml/route.ts`, and are advertised through `alternates.types` in `app/layout.tsx` and a `Link:` header in `public/_headers`. `scripts/check-out.mjs` resolves every advertised same-origin URL against `out/` after a build. The protocol documentation has its own pair on `docs.decdn.org`, served by Mintlify. +- **Live testnet stats.** The hero terminal and the Contact fleet panel render a loading state into the static HTML and fill in after mount from `https://data.decdn.org/stats-421614.json` (rewritten every five minutes by the `decdn/stats` indexer). The fetch lives in `lib/stats.ts` / `lib/use-stats.ts`; the CSP in `public/_headers` allowlists the origin under `connect-src`, so change `STATS_ORIGIN` and the CSP together. ## Deploy From b1471c3e6cea9f352ae649b7c0fd237702640f74 Mon Sep 17 00:00:00 2001 From: Yigit Gurbulak Date: Fri, 9 Oct 2026 02:50:46 +0300 Subject: [PATCH 2/3] docs: resync docs.decdn.org with the current protocol adrs Co-Authored-By: Claude Opus 5.5 --- docs/docs.json | 2 +- docs/overview/architecture.mdx | 2 +- docs/overview/glossary.mdx | 62 +++++++++++++++++----------------- docs/overview/how-it-works.mdx | 10 +++--- docs/overview/introduction.mdx | 2 +- docs/overview/participants.mdx | 19 ++++++----- docs/protocol/governance.mdx | 22 ++++++------ docs/protocol/network.mdx | 8 ++--- docs/protocol/payments.mdx | 18 +++++++--- docs/protocol/takedown.mdx | 2 +- 10 files changed, 79 insertions(+), 68 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index 8cd5c23..0d01f5e 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -2,7 +2,7 @@ "$schema": "https://mintlify.com/docs.json", "theme": "maple", "name": "deCDN", - "description": "Decentralized CDN — staked nodes cache and serve content over QUIC, paid per-MB via off-chain USDC channels.", + "description": "Decentralized CDN — staked nodes cache and serve content over QUIC, paid per-MB with off-chain vouchers against a shared on-chain USDC pool.", "colors": { "primary": "#0f9d6a", "light": "#1dc889", diff --git a/docs/overview/architecture.mdx b/docs/overview/architecture.mdx index d746de1..0140d26 100644 --- a/docs/overview/architecture.mdx +++ b/docs/overview/architecture.mdx @@ -58,7 +58,7 @@ See [Participants](/overview/participants) for a deeper breakdown per role. - No external origin URL exists — content enters the network through origin-backed nodes whose backends are hidden. - A node cannot earn without delivering verifiable bytes — hash mismatch voids payment. -- A node cannot join the mesh without bonding — registration enforces a capacity bond (`bond = k × Mbps^α`) scaled to declared bandwidth, rather than a flat minimum. +- A node cannot join the mesh without bonding — registration enforces a capacity bond scaled to declared bandwidth (`k × Mbps^α`) and never below the global `minBond` entry floor. - A node cannot register an identity it does not control — both keys must sign at registration. - Safety bounds on all governable parameters are hardcoded — governance cannot set fees to 100% or push the operator revenue share below its floor. - Being recognized as an origin is gated on-chain — governance vets a publisher wallet, and the vetted publisher seats its own origin operators per namespace. Configuring an origin backend locally without being seated means the node's bytes are served only as cache. diff --git a/docs/overview/glossary.mdx b/docs/overview/glossary.mdx index 7ea0f99..01126b2 100644 --- a/docs/overview/glossary.mdx +++ b/docs/overview/glossary.mdx @@ -3,34 +3,34 @@ title: Glossary description: Definitions for the core deCDN terms — bao verified streaming, blobs, capacity bonds, payment pools, vouchers, namespaces, probes, nodes, and clients. --- -| Term | Definition | -| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Bao** | The BLAKE3 verified-streaming encoding used for all delivery. Interleaves content with hash proofs so any requested range verifies against the root once aligned to its enclosing 16 KiB chunk-group boundary. | -| **Blob** | A content-addressed byte sequence. Every blob is identified by its hash; clients verify received bytes against the known hash. | -| **Bond (capacity)** | TOKEN locked in `CapacityBond` proportional to a node's declared bandwidth (`bond = k × Mbps^α`) — the only TOKEN-side requirement to join the mesh; the bond scales with declared capacity rather than a flat minimum. Returned on deregistration after an unbonding period. | -| **Capability** | An owner-signed authorization letting one key spend up to a cap from a pool until an expiry. Node-agnostic — valid at every node — and registered on-chain once, on that signer's first redemption. | -| **Chain root** | The head of a hash chain committed in a voucher. The payer releases one **preimage** per megabyte delivered, advancing the amount owed without another signature; a preimage proves its own position by hashing forward to the root. | -| **Client** | A lightweight QUIC endpoint that streams content and pays per MB. No stake, no on-chain registration, no network role. | -| **Coverage** | Which 64 MiB blocks of a blob a holder will serve, carried as a bitmap in DHT records and probe responses. Lets a requester route a wanted range to a holder that actually has it. | -| **Credit window** | The unpaid balance a node will carry on one lane before it pauses delivery. Starts at a floor and ramps as the lane pays, bounding what a non-paying counterparty can extract. | -| **DHT** | Kademlia-subset content discovery. Nodes self-publish records, with coverage, when caching a blob. | -| **Full holder** | A node caching a complete blob, as opposed to a partial holder caching only some blocks. Both are eligible sources for a multi-source fetch. | -| **Hash sequence** | An ordered collection of blob hashes (a directory or manifest equivalent). | -| **Lane** | The payment relationship between one signer and one node: an independent running total with its own watermark, hash chain, and credit window. A pool backs many lanes at once. | -| **Multi-source fetch** | Fetching one large blob from several holders in parallel, each serving disjoint segments, up to a configurable maximum (`max_sources`) and at most one node per operator. Each source is an ordinary paid stream — no new wire protocol. | -| **Namespace** | A publisher-owned identifier for a content set, and the unit of origin addressing. A request pairs it with the blob hash; the default namespace names no publisher and no authorized origins. | -| **Node** | A bonded QUIC endpoint that caches and delivers blobs. Some are origin-backed; others are pure caches. | -| **Origin-backed node** | A node configured with an S3-compatible store, NFS mount, or local disk — the canonical source for specific blobs. Never experiences a true cache miss for blobs in that store; for anything else it is a cache node. | -| **Origin backend** | The opaque storage behind an origin-backed node (S3, R2, B2, MinIO, NFS, local disk). Never exposed to the network. | -| **Outboard** | A blob's BLAKE3 hash tree, stored separately from its content. An origin-cold node must import or build the outboard before it can serve verified ranges. | -| **Payment chunk** | The payment quantum: 1 MiB, a protocol constant. One released preimage pays for exactly one chunk. Distinct from the 16 KiB bao chunk group, which is the unit of verification. | -| **Peer mesh** | The flat set of all bonded nodes. Membership comes from the on-chain registry's active set; content location comes from the DHT. | -| **Pool (payment)** | One on-chain USDC deposit that backs off-chain vouchers from many capped signers to many nodes. Opened once by its owner and reused; the owner reclaims the unspent remainder at close, after a grace window that lets nodes redeem first. | -| **Probe** | Parallel latency, availability and coverage check that runs before any delivery commitment. Its signed fields are on-chain slash evidence. | -| **Publisher** | An address that owns at least one namespace. Once governance has vetted the publisher wallet, it seats and unseats the origin operators for its own namespaces directly. | -| **Segment** | A contiguous, verifiably-aligned byte range assigned to a single source during a multi-source fetch. Independently verifiable, so it can be split or reassigned without re-reading from the start. | -| **Selection score** | The ranking a node uses to pick an upstream holder on a cache miss, combining advertised price, observed latency, and its local reputation score. Lower is better. Clients do not use it: they rank by measured round-trip time alone. | -| **Slashing window** | The 30 seconds for which a node's last probe-quoted rate is binding. A stream opened inside it must be served at or below that quote; serving higher is slashable. | -| **TOKEN** | The deCDN bonding/governance token: locked as a node's capacity bond and burned by the buyback sink. Not used for payments; governance weight comes from delivered bytes, not TOKEN balance. | -| **USDC** | The payment currency for all delivery. Fixed at deployment; pools deposit, voucher, and settle in USDC. | -| **Voucher** | A signed off-chain payment message naming its payee node and carrying a cumulative amount owed. The highest amount settles; a re-submitted voucher pays zero. | +| Term | Definition | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Bao** | The BLAKE3 verified-streaming encoding used for all delivery. Interleaves content with hash proofs so any requested range verifies against the root once aligned to its enclosing 16 KiB chunk-group boundary. | +| **Blob** | A content-addressed byte sequence. Every blob is identified by its hash; clients verify received bytes against the known hash. | +| **Bond (capacity)** | TOKEN locked in `CapacityBond` proportional to a node's declared bandwidth (`bond = k × Mbps^α`) — the only TOKEN-side requirement to join the mesh. At entry it must also meet the global `minBond` floor. Returned on deregistration after an unbonding period. | +| **Capability** | An owner-signed authorization letting one key spend up to a cap from a pool until an expiry. Node-agnostic — valid at every node — and registered on-chain once, on that signer's first redemption. Write-once: the first registration fixes the cap and expiry, and changing the terms takes a new signer key. | +| **Chain root** | The head of a hash chain committed in a voucher. The payer releases one **preimage** per megabyte delivered, advancing the amount owed without another signature; a preimage proves its own position by hashing forward to the root. | +| **Client** | A lightweight QUIC endpoint that streams content and pays per MB. No stake, no on-chain registration, no network role. | +| **Coverage** | Which 64 MiB blocks of a blob a holder will serve, carried as a bitmap in DHT records and probe responses. Lets a requester route a wanted range to a holder that actually has it. | +| **Credit window** | The unpaid balance a node will carry on one stream before it pauses delivery. Starts at one chunk and ramps with the stream's confirmed payment plus the credit it carries from its lane, bounding what a non-paying counterparty can extract. | +| **DHT** | Kademlia-subset content discovery. Nodes self-publish records, with coverage, once they hold at least one verified 64 MiB block of a blob. | +| **Full holder** | A node caching a complete blob, as opposed to a partial holder caching only some blocks. Both are eligible sources for a multi-source fetch. | +| **Hash sequence** | An ordered collection of blob hashes (a directory or manifest equivalent). | +| **Lane** | The payment relationship between one signer and one node: an independent running total with its own watermark, hash chain, and ramp-credit pool shared by its streams. A pool backs many lanes at once. | +| **Multi-source fetch** | How every client fetch runs: one scheduler pulling disjoint segments from the admitted holders in parallel (a lane set of one when a single holder answers), up to a configurable maximum (`max_sources`) and at most one node per operator. Each source is an ordinary paid stream — no new wire protocol. | +| **Namespace** | A publisher-owned identifier for a content set, and the unit of origin addressing. A request pairs it with the blob hash; the default namespace names no publisher and no authorized origins. | +| **Node** | A bonded QUIC endpoint that caches and delivers blobs. Some are origin-backed; others are pure caches. | +| **Origin-backed node** | A node configured with an S3-compatible store, NFS mount, or local disk — the canonical source for specific blobs. Never experiences a true cache miss for blobs in that store; by default it declines anything else rather than relaying it. | +| **Origin backend** | The opaque storage behind an origin-backed node (S3, R2, B2, MinIO, NFS, local disk). Never exposed to the network. | +| **Outboard** | A blob's BLAKE3 hash tree, stored separately from its content. An origin-cold node must import or build the outboard before it can serve verified ranges. | +| **Payment chunk** | The payment quantum: 1 MiB, a protocol constant. One released preimage pays for exactly one chunk. Distinct from the 16 KiB bao chunk group, which is the unit of verification. | +| **Peer mesh** | The flat set of all bonded nodes. Membership comes from the on-chain registry's active set; content location comes from the DHT. | +| **Pool (payment)** | One on-chain USDC deposit that backs off-chain vouchers from many capped signers to many nodes. Opened once by its owner and reused; the owner reclaims the unspent remainder at close, after a grace window that lets nodes redeem first. | +| **Probe** | Parallel latency, availability and coverage check that runs before any delivery commitment. Its signed fields are on-chain slash evidence. | +| **Publisher** | An address that owns at least one namespace. Once governance has vetted the publisher wallet, it seats and unseats the origin operators for its own namespaces directly. | +| **Segment** | A contiguous, verifiably-aligned byte range assigned to a single source during a multi-source fetch. Independently verifiable, so it can be split or reassigned without re-reading from the start. | +| **Selection score** | The ranking a node uses to pick an upstream holder on a cache miss, combining advertised price, observed latency, and its local reputation score. Lower is better. Clients do not use it: they rank by measured round-trip time alone. | +| **Slashing window** | The 30 seconds for which a node's last probe-quoted rate is binding. A stream opened inside it must be served at or below that quote; serving higher is slashable. | +| **TOKEN** | The deCDN bonding/governance token: locked as a node's capacity bond and burned by the buyback sink. Not used for payments; governance weight comes from delivered bytes, not TOKEN balance. | +| **USDC** | The payment currency for all delivery. Fixed at deployment; pools deposit, voucher, and settle in USDC. | +| **Voucher** | A signed off-chain payment message naming its payee node and carrying a cumulative amount owed. The highest amount settles; a re-submitted voucher pays zero. | diff --git a/docs/overview/how-it-works.mdx b/docs/overview/how-it-works.mdx index 0f239a3..e446dd9 100644 --- a/docs/overview/how-it-works.mdx +++ b/docs/overview/how-it-works.mdx @@ -47,18 +47,20 @@ sequenceDiagram Paid delivery is one protocol — used both client→node and node→node. Every byte of the node-to-node pull is paid by the serving node, which amortizes that cost across many downstream client deliveries. The pull is **demand-windowed**: the node fills only far enough ahead of what its client has consumed and paid for, so a client that abandons at half leaves the node holding the demanded prefix rather than the whole blob. -A node may also simply decline. Refusing is not a protocol violation and is not slashable: if the upstream price doesn't clear the margin on the resale, serving at a loss is the wrong move for the operator and the client re-routes to another provider, paying only a latency cost. +A node may also simply decline. Refusing is not a protocol violation and is not slashable: if the upstream price doesn't clear the margin on the resale, serving at a loss is the wrong move for the operator and the client re-routes to another provider, paying only a latency cost. Each node gates its buys on a node-local ceiling (`max_buy`): it buys cold content at the market price while the upstream source still has a bounded **warming allowance**, then demands a proven margin, and answers a below-margin miss with an ordinary `NotFound`. ## Multi-source parallel fetch -For a large blob, the client doesn't pull from a single node. A client-side scheduler admits a set of holders — **full and partial holders alike**, ranked by measured round-trip time, at most one node per operator — and fans out to all of them at once, up to `max_sources` (default 4, above a 64 MiB size floor). +Every client fetch runs through one scheduler. It admits a set of holders — **full and partial holders alike**, ranked by measured round-trip time, at most one node per operator — up to `max_sources` (default 4), and each holder's lane joins as soon as its source is usable. A blob with a single admissible holder simply gets a lane set of one; there is no separate single-source path. -Assignment follows **coverage**: probes carry a bitmap of which 64 MiB blocks each holder will serve, so each block goes to a holder that already has it. A block no admitted holder covers is a genuine gap, warmed once from origin — and that warmer's new coverage becomes discoverable supply for the next fetch. +Assignment follows **coverage**: probes carry a bitmap of which 64 MiB blocks each holder will serve, and a lane takes the blocks it covers first. Coverage is a preference, not an exclusive claim — when no running lane covers a pending block, any running lane takes it. A block no admitted holder covers is a genuine gap, warmed once from origin — and that warmer's new coverage becomes discoverable supply for the next fetch. -Assignment is dynamic. When a source finishes its **segment**, it steals the largest remaining one from a peer, splitting it on a verifiable boundary; every segment is verified as it lands, and a stalled or failed source's _unfinished remainder_ is reassigned without restarting the download. There is deliberately no hedging — racing a segment against idle sources would mean paying for the copies that lose, so the tail is bounded by deadline-based reassignment instead. Aggregate throughput therefore scales with the number of admitted sources rather than any one holder's uplink. +Assignment is dynamic. When a source finishes its **segment**, it steals the tail of the range whose covered part has the most unfetched bytes, splitting it on a verifiable boundary in proportion to each source's delivery rate (ranges under 16 MiB are not split, and the victim always keeps at least 4 MiB ahead of its frontier). A freed source may also take the tail of an uncovered range from a **slow victim** — one delivering at a quarter of its own rate or less — serving it by pull-through. Every segment is verified as it lands. A source with no verified progress for 10 s, or one that drops or errors, cools (2 s, doubling to 60 s, reset by a verified byte) and rejoins; only its _unfinished remainder_ is reassigned, and the download never restarts. There is deliberately no hedging — racing a segment against idle sources would mean paying for the copies that lose, so the tail is bounded by reassignment and slow-victim steals instead. Aggregate throughput therefore scales with the number of admitted sources rather than any one holder's uplink. There is no new wire surface — each source is just an ordinary paid stream, so pools, vouchers, and slashing work exactly as in the single-source paths above. Every source draws on the client's one pool, on its own independent lane. +A fetch with no usable source waits for one rather than failing. It stops only on a fault the user must fix (such as an unhealable voucher rejection), a unanimous verdict from the sources, or the stop policy: an interactive session waits indefinitely, while a script gives up after 10 minutes with no verified byte anywhere in the fetch. + ## Payments in four lines 1. **Open** — the payer escrows one USDC deposit in the `PaymentPool` contract. Once, off the fetch path, reused for every node. diff --git a/docs/overview/introduction.mdx b/docs/overview/introduction.mdx index 913d7f9..3142389 100644 --- a/docs/overview/introduction.mdx +++ b/docs/overview/introduction.mdx @@ -10,7 +10,7 @@ deCDN is a **decentralized content delivery network**. Instead of a single provi - **Content-addressed.** Every blob is identified by its BLAKE3 hash. Clients verify the bytes they receive — they do not trust any node to return the right content. - **Paid per megabyte.** Every byte delivered is paid, including node-to-node cache-miss pulls. Payments are off-chain vouchers denominated in USDC (fixed at deployment), backed by one shared on-chain pool that a payer opens once and reuses for every node it buys from. - **Origin-hidden.** Origin backends (S3, R2, B2, NFS, local disk) are opaque per-node configuration. No external origin URL is ever exposed to the network. -- **Bonded and slashable.** Nodes post a TOKEN capacity bond (scaled to declared bandwidth) to join the peer mesh. Slashable offenses have on-chain evidence paths that anyone can challenge. +- **Bonded and slashable.** Nodes post a TOKEN capacity bond — scaled to declared bandwidth, and never below a global entry floor — to join the peer mesh. Slashable offenses have on-chain evidence paths that anyone can challenge. - **No single operator.** The on-chain registry for node discovery. A DHT for content location. Signed protocol messages as on-chain slash evidence. Operator governance weighted by delivered bytes, with a timelock for parameter updates. ## What it isn't diff --git a/docs/overview/participants.mdx b/docs/overview/participants.mdx index 5ead605..b5f0278 100644 --- a/docs/overview/participants.mdx +++ b/docs/overview/participants.mdx @@ -10,10 +10,11 @@ deCDN has two protocol roles (nodes and clients) plus one external component (or A **node** is a bonded QUIC endpoint that caches and serves content-addressed blobs. - **Identity.** A network keypair bound on-chain to an Ethereum address at registration ([network](/protocol/network)). -- **Bond.** A capacity bond in TOKEN, scaled to declared bandwidth (`bond = k × Mbps^α`) — the only TOKEN-side requirement to join the mesh; the bond scales with declared capacity rather than a flat minimum. +- **Bond.** A capacity bond in TOKEN, scaled to declared bandwidth (`bond = k × Mbps^α`) — the only TOKEN-side requirement to join the mesh, and never below the global `minBond` entry floor. - **Discovery.** Registered on-chain with its addresses and region; publishes a DHT record for each blob it caches, and quotes its current rate in every signed probe response. - **Delivery.** Serves the paid-delivery protocol — the sole rail for both client→node and node→node traffic. - **Earnings.** Per-MB voucher revenue in USDC. +- **Cache policy.** Node-local and pluggable: admission defaults to `always` and eviction to `lru`, with W-TinyLFU available for either. Pins and probe-triggered holds are engine-enforced, and a deny-listed hash is evicted even when pinned. Two deployment shapes: @@ -22,18 +23,18 @@ Two deployment shapes: | **Pure cache** | No origin backend. Pulls missed blobs from other nodes. | You want to earn on popular content; you don't control any origin. | | **Origin-backed** | Configured with an S3-compatible backend, NFS mount, or local disk. The hash→object-key mapping is a local content catalog. | You are the content provider, or you are running a "last-mile" seed node for a provider. | -"Origin-backed" is a deployment choice, not a protocol distinction — the network cannot tell them apart at runtime. It is also per-blob: for anything outside its own backend, an origin-backed node is a cache node and pays upstream like any other. +"Origin-backed" is a deployment choice, not a protocol distinction — the network cannot tell them apart at runtime. By default an origin-backed node serves only what its own backend can produce and declines anything else as `NotFound`, so the client fails over; an operator can opt in to relaying foreign blobs (`cache.relay_foreign_namespaces`), and then pays upstream like any cache node. ## Clients A **client** is a lightweight QUIC endpoint that consumes content and pays for it. - **Identity.** A network keypair generated at first startup and persistent across sessions, plus a per-connection binding to an Ethereum address that the node discards on disconnect — no stake, no on-chain registration. -- **Discovery.** Reads the on-chain registry for its peer set and queries the DHT for holders. Publishes nothing and holds no network role. +- **Discovery.** Takes candidates from its own peer store or the on-chain registry and probes them; it runs no DHT lookup. Publishes nothing and holds no network role. - **Payment.** Opens one on-chain USDC pool, reuses it for every node it buys from, and signs cumulative off-chain vouchers as bytes flow ([payments](/protocol/payments)). - **Trust model.** Verifies hashes on all bytes received. Trusts no node for content, and none for availability either — a node may decline to serve, and the client simply re-routes without paying for the attempt. -Clients can run a multi-source parallel download for large files, pulling disjoint segments from several holders at once, and resume crashed transfers from the last verified chunk-group boundary. +Every client fetch runs a multi-source scheduler, pulling disjoint segments from up to four holders at once (or from one, when only one holds the blob), and resume crashed transfers from the last verified chunk-group boundary. ## External components @@ -45,8 +46,8 @@ An S3 bucket, R2 bucket, MinIO instance, NFS mount, or local disk. Holds the can ## Who bonds, who pays -| Participant | Bonds? | Pays? | Earns? | -| ------------------ | -------- | -------------------------------- | ------------------------- | -| Pure cache node | ✅ TOKEN | ✅ Pays upstream on cache miss | ✅ Per-MB voucher revenue | -| Origin-backed node | ✅ TOKEN | ✅ For blobs outside its backend | ✅ Per-MB voucher revenue | -| Client | ❌ | ✅ Per-MB voucher | ❌ | +| Participant | Bonds? | Pays? | Earns? | +| ------------------ | -------- | -------------------------------------------------------- | ------------------------- | +| Pure cache node | ✅ TOKEN | ✅ Pays upstream on cache miss | ✅ Per-MB voucher revenue | +| Origin-backed node | ✅ TOKEN | Only if it opts in to relaying blobs outside its backend | ✅ Per-MB voucher revenue | +| Client | ❌ | ✅ Per-MB voucher | ❌ | diff --git a/docs/protocol/governance.mdx b/docs/protocol/governance.mdx index 42d2950..1fc4f90 100644 --- a/docs/protocol/governance.mdx +++ b/docs/protocol/governance.mdx @@ -9,7 +9,7 @@ OpenZeppelin Governor whose voting weight is an operator's **delivered bytes ove ## Voting weight -Voting weight is each operator's **served bytes over a trailing window** (default ~1 quarter, governable), scaled by a tenure ramp and capped per operator (default 5% of the window's bytes). Bytes are counted a week at a time, and each week's count is additionally capped at what the capacity the operator declared that week could physically have delivered. +Voting weight is each operator's **served bytes over a trailing window** (default ~1 quarter, governable), scaled by a tenure ramp and capped per operator (10% of the window's bytes at launch; governance can only lower it). Bytes are counted a week at a time, and each week's count is additionally capped at what the capacity the operator declared that week could physically have delivered. - **Passive TOKEN holders have no vote.** Governance power tracks real delivery, not holdings — you must operate a node and serve bytes to vote. - **Fresh operators ramp in.** A new operator that serves heavily on day one still votes at a fraction of its byte share until the tenure ramp completes over several months. @@ -28,16 +28,16 @@ The phase ends when the multisig judges the operator set broad and diverse enoug Even governance cannot set parameters outside these bounds: -| Parameter | Default | Bounds | -| ----------------------------------------- | ---------------- | -------------------- | -| Operator revenue share (`FeeRouter` base) | 60% | 40% – 90% | -| Burn share (`FeeRouter`) | 30% (0% dormant) | 5% – 50% when active | -| Treasury share (`FeeRouter`) | 10% | 0% – 30% | -| Redemption grace window | 48 h | 48 h – 72 h | -| Per-operator voting cap | 5% | 1% – 25% | -| Trailing vote window | 13 weeks | 4 – 26 weeks | -| Capacity-curve exponent (α) | 1.2 | 1.0 – 1.8 | -| Unbonding window | 14 days | 7 – 60 days | +| Parameter | Default | Bounds | +| ----------------------------------------- | ----------------- | ----------------------- | +| Operator revenue share (`FeeRouter` base) | 60% (90% dormant) | 40% – 90% | +| Burn share (`FeeRouter`) | 30% (0% dormant) | 5% – 50% when active | +| Treasury share (`FeeRouter`) | 10% | 0% – 30% | +| Redemption grace window | 48 h | 48 h – 72 h | +| Per-operator voting cap | 10% | 1% – 10%, decrease-only | +| Trailing vote window | 13 weeks | 4 – 26 weeks | +| Capacity-curve exponent (α) | 1.2 | 1.0 – 1.8 | +| Unbonding window | 14 days | 7 – 60 days | The three `FeeRouter` shares must sum to exactly 100% on every update. The burn share is the one that also accepts **zero**, so the buyback sink can be switched off entirely; what it cannot be is nominally on at a negligible share — either off, or at 5% or more. diff --git a/docs/protocol/network.mdx b/docs/protocol/network.mdx index 72dd99e..1bc20d0 100644 --- a/docs/protocol/network.mdx +++ b/docs/protocol/network.mdx @@ -11,19 +11,19 @@ All staked nodes form a **flat mesh** with no routing hierarchy. Node discovery The on-chain registry's active set is the sole membership source. A node reads it at startup and keeps it fresh by subscribing to registration, address-update, deregistration and auto-ejection events; sub-second L2 block times keep the staleness window small. Clients do the same and re-read periodically, falling back to a cached peer list — with its age surfaced — when the RPC endpoint is unreachable. -The same active set seeds the DHT routing table, so a freshly started node participates in lookups immediately. A node dials any peer by its network identity, which resolves to reachable transport addresses on its own — there is no separate address-exchange layer, and no metadata broadcast. +The same active set seeds the DHT routing table, so a freshly started node participates in lookups immediately. A node dials any peer by its network identity, which resolves to reachable transport addresses from two sources: the registry's `multiaddrs` as direct-address hints, and each node's signed pkarr record carrying its relay URL and public IPs. There is no separate address-exchange layer, and no metadata broadcast. ## Content discovery A cache miss issues a DHT lookup for the hash, which returns candidate holders along with a coarse **coverage bitmap** per holder — so a requester can prune candidates that do not hold the range it wants. Candidates are then probed in parallel to confirm live availability, fresh coverage, latency and price. Collection stops as soon as enough blob-holding candidates answer to fill the failover budget, and otherwise at a 500 ms ceiling that accommodates inter-continental round trips. -When a lookup returns no providers for a request that names a namespace, the namespace's on-chain origin set is the deterministic fallback. A request naming no namespace has no directory to fall back to — it has no authorized origins — so a miss there simply fails. Either way, if nothing answers, the blob is not available in the network. +When a lookup returns no providers — or only providers that do not cover every block — for a request that names a namespace, the namespace's on-chain origin set is the deterministic fallback. A node checks that its candidates cover every block of the range before it commits to its own client; if any block has none, it refuses with a signed miss so the client fails over, rather than starting a stream that ends short. A request naming no namespace has no directory to fall back to — it has no authorized origins — so a miss there simply fails. Either way, if nothing answers, the blob is not available in the network. ## Selection Clients and nodes rank candidates differently. -- **Clients** take candidates from their own peer store or the registry, not the DHT, and rank them by measured round-trip time alone. Price is not a rank key: the client pays the rate the node signs and can refuse a quote above a ceiling of its own. A client keeps no reputation score; it only sets aside, for a few minutes, a node that just failed it. +- **Clients** take candidates from their own peer store or the registry, not the DHT, and rank them by measured round-trip time alone. Price is not a rank key: the client pays the rate the node signs and can refuse a quote above a ceiling of its own. A client keeps no reputation score. Within a fetch, a node that fails it cools for 2 s, doubling to 60 s and reset by a verified byte; across runs, a failure stamp keeps it out of the peer-store candidate set for a fixed window. - **Nodes** pulling on a cache miss rank holders by a unified score combining advertised price, observed latency, and their local reputation score, with a quadratic reputation penalty. Lower is better. ## On-chain registration @@ -38,4 +38,4 @@ Regions inform selection rather than partition the mesh: a client in Frankfurt u ## NAT traversal -The transport handles hole-punching automatically; nodes update their registered addresses when their public-facing address changes. When direct P2P fails, traffic routes through a stateless, content-blind QUIC relay. Relays are not protocol participants — they cannot inspect, cache, or modify content — and they are not an incentivized network role: paying relays per byte would create a perverse incentive to prevent direct connections. Deployments self-host them as operational infrastructure, funded from the protocol treasury or from node staking fees. +The transport handles hole-punching automatically. Registration defaults to the host's public address, and a node behind NAT registers none. When the public address changes, the operator runs `decdn node update-multiaddrs` — the node does not submit the update itself — and the node's pkarr record carries the new address in the meantime. When direct P2P fails, traffic routes through a stateless, content-blind QUIC relay. Relays are not protocol participants — they cannot inspect, cache, or modify content — and they are not an incentivized network role: paying relays per byte would create a perverse incentive to prevent direct connections. Deployments self-host them as operational infrastructure, funded from the protocol treasury or from node staking fees. diff --git a/docs/protocol/payments.mdx b/docs/protocol/payments.mdx index d92f5be..f68241a 100644 --- a/docs/protocol/payments.mdx +++ b/docs/protocol/payments.mdx @@ -34,12 +34,14 @@ sequenceDiagram ## Capabilities -The pool owner authorizes each voucher signer with an off-chain **capability** signed over `{signer, spending_cap, pool_id, expiry}`. It is **node-agnostic** — one capability is valid at every node — and it is registered on-chain once, on that signer's first redemption. +The pool owner authorizes each voucher signer with an off-chain **capability** signed over `{signer, spending_cap, pool_id, expiry}`. It is **node-agnostic** — one capability is valid at every node — and it is registered on-chain once, on that signer's first redemption. Registration is **write-once**: the first capability to land for a signer fixes its `cap` and `expiry`, and the contract ignores every later one. There is no renewal — to change the terms, the owner authorizes a new signer key. -Delegation bounds the damage a compromised key does: `spending_cap` bounds what the key can authorize, `expiry` retires it if the owner doesn't renew. The owner keeps sole control of top-up and close. A single user makes its own key the sole signer; a client fanning out to per-device or per-session keys issues each a small capped capability from one pool. +Delegation bounds the damage a compromised key does: `spending_cap` bounds what the key can authorize, `expiry` retires it. The owner keeps sole control of top-up and close. A single user makes its own key the sole signer; a client fanning out to per-device or per-session keys issues each a small capped capability from one pool. The sum of the caps may exceed the deposit. Nodes keep a pool solvent by reserving a refundable minimum remaining balance and stopping service before the pool reaches it. +At admission a node reads the pool and the signer's on-chain `{cap, expiry, spent}` once, on the connection-open path. A signer whose registration has expired, or whose remaining `cap − spent` — shared across every node — cannot cover a serve floor, is refused before any byte ships. If the headroom runs out mid-stream, the node stops the stream with `SignerCapExhausted` rather than deliver bytes it could never redeem. + ## Vouchers Off-chain signed messages over `{pool_id, signer, provider, amount, bytes_delivered, chain_root, chunk_price}`. Two properties matter: @@ -63,12 +65,18 @@ The payment quantum is the **payment chunk**: `chunk_bytes` = **1 MiB (1,048,576 ## Credit window -Payments from one signer to one node out of one pool form a **lane**: an independent running total, with its own watermark and its own chain. A node's exposure on a lane is bounded by a **credit window** — the unpaid balance it will carry before it pauses — not by the meter's resolution. The window starts at a floor and ramps as the lane pays, so a new payer proves itself on small credit before a node extends more. +Payments from one signer to one node out of one pool form a **lane**: an independent running total, with its own watermark and its own chain. A node's exposure is bounded by a **credit window** — the unpaid balance a stream may carry before the node pauses it — not by the meter's resolution: + +```text +window = min(credit_max, max(chunk_bytes, paid / credit_ramp_divisor)) +``` + +The window is per stream and starts at one chunk, so a new payer proves itself on small credit before a node extends more. `paid` counts the stream's own confirmed payment plus the ramp credit it carries from its lane: each lane keeps an in-memory ramp-credit pool that a new stream takes whole, a fully paid stream returns, and an unpaid one forfeits. A payer pulling range after range keeps its ramp; one that abandons stream after stream gets no more unpaid bytes than its paid total allows. The pool does not survive a node restart. `credit_max` (default 64 MiB) and `credit_ramp_divisor` (default 2) are node-local configuration, not wire or governance parameters. ## Rate discovery -Each node advertises its own per-MB rate, quoted in every signed probe response. Governance sets a **floor**, not a ceiling: a node whose configured rate falls below the current floor raises its quote to the floor and logs a warning rather than refusing to serve, and redemption clamps the byte credit on any voucher priced under it. No governance ceiling caps a node's rate from above: the buyer sees the signed rate before it pays anything, which is what makes a ceiling unnecessary. A requester rejecting an expensive quote is local policy rather than a protocol rule. A quoted rate is binding for any stream opened inside the [slashing window](/protocol/slashing#the-slashing-window). +Each node advertises its own per-MB rate, quoted in every signed probe response. Governance sets a **floor**, not a ceiling: a node advertises its configured rate verbatim and never raises its quote to the floor. A voucher priced under the floor still settles its full amount; redemption clamps only its byte credit toward voting weight, so a below-floor node keeps its revenue and loses only vote weight on those bytes. No governance ceiling caps a node's rate from above: the buyer sees the signed rate before it pays anything, which is what makes a ceiling unnecessary. A requester rejecting an expensive quote is local policy rather than a protocol rule; the only upper bound is the wire constant `MAX_RATE_PER_MB`. A quoted rate is binding for any stream opened inside the [slashing window](/protocol/slashing#the-slashing-window). ## Minimum deposit -`openPool` enforces a governance-set `minDeposit` on the credited deposit. It is a Sybil knob: the per-pool solvency floor bounds loss within one pool, and an attacker escapes it by fanning out across many pools, so the floor taxes the capital locked in simultaneous pools. Deposits stay fully refundable, and the parameter is bounded at **0 to $100** — zero leaves it dormant, and the ceiling keeps governance from pricing small honest buyers out of opening a pool. +`openPool` enforces a governance-set `minDeposit` on the credited deposit. It is a Sybil knob: the per-pool solvency floor bounds loss within one pool, and an attacker escapes it by fanning out across many pools, so the floor taxes the capital locked in simultaneous pools. Deposits stay fully refundable, and the parameter is bounded at **0 to $100** and launches at 0 — zero leaves it dormant, and the ceiling keeps governance from pricing small honest buyers out of opening a pool. diff --git a/docs/protocol/takedown.mdx b/docs/protocol/takedown.mdx index d5ac716..315fb91 100644 --- a/docs/protocol/takedown.mdx +++ b/docs/protocol/takedown.mdx @@ -70,4 +70,4 @@ Each node maintains a **local denylist** for direct legal notices (e.g., a DMCA ## Slash schedule -Blacklist violations use a fixed escalating ladder — **5% / 15% / 50%** for first / second / third offense, capped at 50% per offense. Offenses accumulate over the operator's lifetime; a node whose bond falls below 50% of the minimum is auto-ejected. +Blacklist violations use a fixed escalating ladder — **5% / 15% / 50%** for first / second / third offense, capped at 50% per offense. Offenses accumulate over the operator's lifetime; a node whose bond falls below 50% of the entry floor it activated under (`bondFloorAtActivation`) is auto-ejected. The ejection is recoverable: re-bonding to the live `minBond` and re-registering clears it, unless governance has also blacklisted the operator. From 78980f0d19e915e40df460bba0986c7ba5c1e9db Mon Sep 17 00:00:00 2001 From: Yigit Gurbulak Date: Fri, 9 Oct 2026 04:23:06 +0300 Subject: [PATCH 3/3] docs: fix subject-verb agreement in participants Co-Authored-By: Claude Opus 4.8 --- docs/overview/participants.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/overview/participants.mdx b/docs/overview/participants.mdx index b5f0278..0197f57 100644 --- a/docs/overview/participants.mdx +++ b/docs/overview/participants.mdx @@ -34,7 +34,7 @@ A **client** is a lightweight QUIC endpoint that consumes content and pays for i - **Payment.** Opens one on-chain USDC pool, reuses it for every node it buys from, and signs cumulative off-chain vouchers as bytes flow ([payments](/protocol/payments)). - **Trust model.** Verifies hashes on all bytes received. Trusts no node for content, and none for availability either — a node may decline to serve, and the client simply re-routes without paying for the attempt. -Every client fetch runs a multi-source scheduler, pulling disjoint segments from up to four holders at once (or from one, when only one holds the blob), and resume crashed transfers from the last verified chunk-group boundary. +Every client fetch runs a multi-source scheduler, pulling disjoint segments from up to four holders at once (or from one, when only one holds the blob), and resumes crashed transfers from the last verified chunk-group boundary. ## External components