diff --git a/docs/overview/architecture.mdx b/docs/overview/architecture.mdx index c758b5d..e194582 100644 --- a/docs/overview/architecture.mdx +++ b/docs/overview/architecture.mdx @@ -1,6 +1,6 @@ --- title: Architecture -description: The node, client, and origin-backend roles, plus the full path of a paid delivery — discovery, selection, channel open, streamed bytes, vouchers. +description: The node, client, and origin-backend roles, plus the full path of a paid delivery — discovery, selection, streamed bytes, vouchers, redemption. --- ## System diagram @@ -32,30 +32,27 @@ graph TD C3 -->|"paid delivery"| N3 N2 -.->|opaque fetch| S3 - - N1 <-.->|"gossip"| N2 - N2 <-.->|"gossip"| N3 ``` ## Participant roles -| Role | Operated by | Participates in CDN? | -| ------------------------ | ------------------------------------------------ | --------------------------------------------------------- | -| **Node (pure cache)** | Independent node operator | Yes — bonds, gossips, probes, delivers | -| **Node (origin-backed)** | Content provider _or_ node operator with storage | Yes — same protocol, plus a DAO-recognized origin backend | -| **Client** | End-user or application | Pays for bytes; subscribes to gossip but does not publish | +| Role | Operated by | Participates in CDN? | +| ------------------------ | ------------------------------------------------ | ----------------------------------------------------------------------------- | +| **Node (pure cache)** | Independent node operator | Yes — bonds, probes, publishes to the DHT, delivers | +| **Node (origin-backed)** | Content provider _or_ node operator with storage | Yes — same protocol, plus authorization to serve as an origin for a namespace | +| **Client** | End-user or application | Pays for bytes; reads the registry but holds no role | See [Participants](/overview/participants) for a deeper breakdown per role. ## Life of a paid delivery -1. **Client bootstrap** — client generates a keypair, queries the on-chain registry, seeds a peer table, and subscribes to regional + global gossip topics. -2. **Discovery** — client probes its known peers for a blob hash. On miss, any peer performs a DHT lookup to locate holders. -3. **Selection** — returned candidates are scored by price, latency, and reputation; lowest cost wins. -4. **Payment channel open** — client opens a USDC payment channel with the selected node on-chain ([payments](/protocol/payments)). -5. **Streaming + vouchers** — node streams to the client; client signs off-chain vouchers as bytes flow. -6. **Cache-miss fan-out** — if the node doesn't have the blob, it pulls from another peer (paid). Every byte delivered in the network is paid. -7. **Channel close** — either party initiates settlement on-chain, with a dispute window for stale closes. +1. **Client bootstrap** — client generates a keypair, queries the on-chain registry for the active node set, and caches it. +2. **Discovery** — a DHT lookup locates holders of the blob hash, and a parallel probe confirms which of them will actually serve the wanted range, at what rate and latency. +3. **Selection** — returned candidates are scored by price, latency, and reputation into one selection score; the lowest score wins, so a cheap or nearby node can outrank a better-reputed one. +4. **Payment** — the client pays from a USDC pool it opened once, off the fetch path, and reuses for every node ([payments](/protocol/payments)). +5. **Streaming + vouchers** — node streams to the client; client signs a cumulative voucher and, on an optional hash chain, releases one preimage per 1 MiB chunk as bytes flow. +6. **Cache-miss fan-out** — if the node doesn't have the blob, it pulls from other holders (paid). Every byte delivered in the network is paid. +7. **Redemption** — the node redeems its own vouchers on-chain whenever it likes; the owner closes the pool to reclaim the unspent remainder. ## Key invariants @@ -64,7 +61,7 @@ See [Participants](/overview/participants) for a deeper breakdown per role. - 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 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 DAO-governed — configuring an origin backend locally without authorization means the node's bytes are served only as cache. +- 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. - A node cannot serve a blacklisted hash after the compliance window — doing so is slashable. ## Non-goals @@ -74,3 +71,4 @@ See [Participants](/overview/participants) for a deeper breakdown per role. - Search, discovery, or recommendation - Mobile or web clients - Erasure coding — full replication only +- Multi-chain settlement — a single L2 diff --git a/docs/overview/design-decisions.mdx b/docs/overview/design-decisions.mdx index 73ebcec..3d9ffd5 100644 --- a/docs/overview/design-decisions.mdx +++ b/docs/overview/design-decisions.mdx @@ -1,9 +1,9 @@ --- title: Design decisions -description: The accepted architecture decision records that define deCDN — topology, content addressing, payments, reputation, governance, slashing, tokenomics. +description: The architecture decision records that define deCDN — topology, content addressing, payments, reputation, governance, slashing, tokenomics. --- -The protocol is shaped by a set of accepted ADRs. The table below names each one. Full text is not yet public; numbering gaps reflect superseded or withdrawn proposals. +The protocol is shaped by a set of ADRs, which together are its canonical specification. The table below names each one; individual records range from draft to accepted. Full text is not yet public, and numbering gaps reflect superseded or withdrawn proposals. | ADR | Decision | | --- | ------------------------------ | @@ -18,7 +18,6 @@ The protocol is shaped by a set of accepted ADRs. The table below names each one | 012 | Client architecture | | 013 | Schema evolution | | 014 | On-chain slashing verification | -| 015 | QUIC 0-RTT | | 016 | Smart contract interaction | | 017 | Privacy | | 018 | Liquidity strategy | @@ -28,8 +27,9 @@ The protocol is shaped by a set of accepted ADRs. The table below names each one | 026 | Tokenomics | | 028 | Slashing appeals | | 030 | Region self-attestation | -| 031 | Content blacklist appeals | | 036 | Served-bytes voting weight | | 037 | Regional proxy warming | | 038 | Bao verified-range streaming | | 039 | Multi-source parallel fetch | +| 040 | Cache admission & eviction | +| 041 | Refuse-to-serve economics | diff --git a/docs/overview/glossary.mdx b/docs/overview/glossary.mdx index 6b9d9c9..a806a21 100644 --- a/docs/overview/glossary.mdx +++ b/docs/overview/glossary.mdx @@ -1,6 +1,6 @@ --- title: Glossary -description: Definitions for the core deCDN terms — bao verified streaming, blobs, capacity bonds, payment channels, vouchers, probes, nodes, and clients. +description: Definitions for the core deCDN terms — bao verified streaming, blobs, capacity bonds, payment pools, vouchers, namespaces, probes, nodes, and clients. --- | Term | Definition | @@ -8,21 +8,29 @@ description: Definitions for the core deCDN terms — bao verified streaming, bl | **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. | -| **Channel (payment)** | On-chain escrow between client and node that backs off-chain vouchers. Opens with a deposit; settles with the highest-nonce voucher after a dispute window. | -| **Client** | A lightweight QUIC endpoint that streams content and pays per MB. No stake, no gossip publish. | -| **DHT** | Kademlia-subset content discovery. Nodes self-publish records when caching a blob. | -| **Full holder** | A node caching a complete blob (as opposed to a partial range). Only full holders are eligible as sources for a multi-source fetch. | -| **Gossip** | Epidemic broadcast over topic channels for node metadata and rate updates. | +| **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). | -| **Multi-source fetch** | Fetching one large blob from several full holders in parallel, each serving bao-aligned work units, up to a configurable maximum (`max_sources`). Each source is an ordinary paid stream — no new wire protocol. | +| **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. | +| **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. | -| **Peer mesh** | The flat set of all bonded nodes. Discovery via gossip; content location via DHT. | -| **Probe** | Parallel latency + availability check that runs before any delivery commitment. | +| **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** | A combined ranking over advertised price, observed latency, and reputation. Lower is better. | +| **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; channels deposit, voucher, and settle in USDC. | -| **Voucher** | A signed off-chain payment message. The primary payment instrument between client and node. | -| **Work unit** | A bao-aligned byte range assigned to a single source during a multi-source fetch. Independently verifiable, so it can be re-dispatched or hedged without re-reading from the start. | +| **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 5bb8873..b87286f 100644 --- a/docs/overview/how-it-works.mdx +++ b/docs/overview/how-it-works.mdx @@ -10,17 +10,18 @@ sequenceDiagram autonumber participant C as Client participant N as Node + participant P as PaymentPool (on-chain) C->>N: Probe(hash) - N-->>C: Probe response (has blob, rate) - C->>N: Open on-chain channel (USDC) - C->>N: Stream request(hash) + N-->>C: Probe response (rate signed; coverage unsigned) + C->>N: Stream request(hash, namespace) N-->>C: Bytes (hash-verified on receipt) - C-->>N: Voucher (signed, off-chain) - C->>N: Channel close / settle + C-->>N: Voucher (signed, cumulative) + C-->>N: Preimage per MiB delivered + N->>P: Redeem (voucher + preimage) ``` -The client picks the candidate with the best combination of price, latency, and reputation. After settling on a node, it opens a USDC channel and streams. Vouchers cover every byte delivered; the node can settle any voucher on-chain later. +The client picks the candidate with the best combination of price, latency, and reputation. It pays from a pool it opened once and reuses for every node, so nothing on this path touches the chain until the node chooses to redeem. Vouchers cover every byte delivered. ## Cache miss with pull-through @@ -30,45 +31,47 @@ sequenceDiagram participant C as Client participant N as Node (serving) participant D as DHT peers - participant P as Peer with blob + participant P as Peers with the blob participant O as Origin-backed node - C->>N: Stream request(hash) + C->>N: Stream request(hash, namespace) N->>D: DHT lookup(hash) - D-->>N: Candidate peers + D-->>N: Candidate holders + coverage N->>P: Probe (parallel) - P-->>N: Has blob + P-->>N: Fresh coverage, rate N->>P: Stream request — N pays P P-->>N: Bytes (verified) N-->>C: Stream bytes while pulling (N charges C) - N->>O: Falls back to origin-backed node if no peer has the blob + N->>O: Falls back to an authorized origin if no holder covers the range ``` -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. +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. -If pull-through is disabled in the node's config, the node returns a redirect pointing to a peer (never an origin URL). The client opens a channel with that peer directly. +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. ## Multi-source parallel fetch -For a large blob, the client doesn't pull from a single node. A client-side scheduler splits the blob across several sources at once — full holders only for now — up to `max_sources` (a configurable cap on parallel sources). Each source is assigned bao-aligned **work units** (byte ranges the bao decoder can verify on their own). +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 the same selection score, 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). -Assignment is dynamic: fast sources steal work from slow ones, every unit is verified as it lands, a failed unit is re-dispatched to another source, and the last few stragglers are hedged across sources so one slow peer can't stall completion. Aggregating parallel streams lets a blob be delivered at multi-gigabit speeds once it's cached across enough full holders — no single origin's uplink caps the transfer. +Assignment follows **coverage**: discovery and 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. -There is no new wire surface — each source is just an ordinary paid `cdn/client/v1` stream, so channels, vouchers, and slashing work exactly as in the single-source paths above. +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. -## Payment channels in four lines +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. -1. **Open** — client opens a channel on-chain. Deposit is escrowed in the `PaymentChannel` contract. -2. **Vouchers** — off-chain signed messages updating the cumulative amount owed. Cadence is negotiable per stream. -3. **Close** — either party submits the latest voucher on-chain. A dispute window lets the counterparty submit a later-nonce voucher if the close is stale. -4. **Cooperative close (fast path)** — when both parties are online and agree, a single co-signed transaction settles the channel immediately with no dispute window, falling back to the standard close if either side declines. +## 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. +2. **Delegate** — the owner signs capped, expiring capabilities for whichever keys may spend from the pool. One capability is valid at every node. +3. **Meter** — a signed cumulative voucher anchors the amount owed, and an optional hash chain advances it by one 1 MiB **payment chunk** per released preimage, with no further signature. A voucher opening no chain is sealed at the amount it signs. +4. **Redeem** — the node settles its own vouchers on-chain whenever it likes; the owner closes the pool to reclaim the remainder, after a grace window that lets nodes redeem first. ## What keeps the network honest -- **Hash verification.** Clients verify every chunk against the known blob hash. A bad byte voids payment. -- **On-chain evidence.** Protocol messages are signed in a form that can be verified on-chain as evidence of phantom announcements, rate manipulation, or blacklist violations ([slashing](/protocol/slashing)). -- **Synchronous adjudication.** The on-chain judge verifies submitted evidence and slashes on the spot, with no counter-evidence window. -- **Reputation.** A 0.0–1.0 score combining direct experience and signed gossip from staked nodes ([reputation](/protocol/reputation)). +- **Hash verification.** Clients verify every chunk group against the known blob hash as it arrives. A bad byte voids payment for that window. +- **On-chain evidence.** Probe and stream responses are signed in a form verifiable on-chain, as evidence of rate manipulation or blacklist violations ([slashing](/protocol/slashing)). +- **Synchronous adjudication.** The on-chain judge verifies revealed evidence and slashes on the spot, with no counter-evidence window. Challenges commit before they reveal, so the challenger reward cannot be front-run. +- **Reputation.** A local 0.0–1.0 score, built from a node's own delivery outcomes and nothing else ([reputation](/protocol/reputation)). - **Content blacklist.** Governance-managed on-chain hash blacklist. Serving a blacklisted hash after the compliance window is slashable ([takedown](/protocol/takedown)). ## Where the boundary sits @@ -76,9 +79,10 @@ There is no new wire surface — each source is just an ordinary paid `cdn/clien | Layer | Trust | | -------------------------- | ------------------------------------------------------------- | | Bytes delivered | **Verified** against known hash | -| Node availability | Trusted — slashed if phantom-announced | -| Node rate honesty | Trusted — slashed if advertised rate contradicts charged rate | +| Node availability | Not trusted — a node may decline; the client re-routes | +| Node rate honesty | Trusted — slashed if the charged rate contradicts its quote | +| Node region claim | Self-attested — penalized in selection when latency disagrees | | Origin backend correctness | **Assumed** — outside protocol scope (operator concern) | -| L2 RPC provider honesty | **Assumed** — mitigated via multi-source bootstrap | +| L2 RPC provider honesty | **Assumed** — mitigated via multiple independent providers | See [privacy](/protocol/privacy) for the adversary model. diff --git a/docs/overview/introduction.mdx b/docs/overview/introduction.mdx index cda3cec..913d7f9 100644 --- a/docs/overview/introduction.mdx +++ b/docs/overview/introduction.mdx @@ -3,15 +3,15 @@ title: Introduction description: deCDN is a decentralized CDN — staked independent nodes cache content and deliver it over peer-to-peer QUIC, paid per megabyte in USDC. --- -deCDN is a **decentralized content delivery network**. Instead of a single provider operating all edge servers, staked independent **nodes** cache content and deliver it to clients over peer-to-peer QUIC connections. Clients pay nodes directly, per megabyte, over off-chain payment channels. +deCDN is a **decentralized content delivery network**. Instead of a single provider operating all edge servers, staked independent **nodes** cache content and deliver it to clients over peer-to-peer QUIC connections. Clients pay nodes directly, per megabyte, with off-chain vouchers backed by a single on-chain deposit. ## Core properties - **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 run on off-chain channels denominated in USDC (fixed at deployment), with on-chain settlement only at channel open and close. +- **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. -- **No single operator.** Gossip for discovery. A DHT for content location. Optimistic challenge-response for disputes. Operator governance weighted by delivered bytes, with a timelock for parameter updates. +- **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 ae9f684..5ead605 100644 --- a/docs/overview/participants.mdx +++ b/docs/overview/participants.mdx @@ -11,7 +11,7 @@ A **node** is a bonded QUIC endpoint that caches and serves content-addressed bl - **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. -- **Gossip.** Publishes node metadata on a global topic and a regional topic. Also emits rate-change updates. +- **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. @@ -22,22 +22,22 @@ 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. +"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. ## Clients A **client** is a lightweight QUIC endpoint that consumes content and pays for it. -- **Identity.** Ephemeral network keypair — no stake, no on-chain registration. -- **Gossip.** Subscribes to topics but does not publish. -- **Payment.** Opens an on-chain USDC payment channel with each node it buys from, then signs off-chain vouchers as bytes flow ([payments](/protocol/payments)). -- **Trust model.** Verifies hashes on all bytes received. Trusts nodes for availability (slashable if claimed and not delivered), not for content. +- **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. +- **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-node parallel download for large files and resume crashed transfers from the last verified byte. +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. ## External components -These are **not CDN protocol participants**. They ride the same QUIC transport but do not gossip, probe, or bond. +These are **not CDN protocol participants**. A node reaches its backend over that backend's own interface — HTTP, the S3 API, a filesystem — not over a CDN protocol. A backend never registers, probes, or bonds. ### Origin backend @@ -45,8 +45,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 | ❌ | ✅ 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 | ✅ For blobs outside its backend | ✅ Per-MB voucher revenue | +| Client | ❌ | ✅ Per-MB voucher | ❌ | diff --git a/docs/protocol/content-addressing.mdx b/docs/protocol/content-addressing.mdx index 5914ecd..6e2783a 100644 --- a/docs/protocol/content-addressing.mdx +++ b/docs/protocol/content-addressing.mdx @@ -1,6 +1,6 @@ --- title: Content addressing -description: Every blob is addressed by its BLAKE3 hash and verified in-stream with bao, so clients never trust a node to return the right bytes. +description: Every blob is addressed by its BLAKE3 hash and verified in-stream with bao, with a namespace naming who is authorized to serve it from origin. --- ## Decision @@ -14,32 +14,38 @@ Every blob is identified by its **BLAKE3 hash**. Clients and nodes verify receiv ## Verified streaming -Delivery uses **bao** — an interleaved verified-stream encoding of the BLAKE3 tree. The `cdn/client/v1` paid-delivery payload is always bao bytes: content interleaved with the hash proofs needed to verify each 16 KiB chunk group against the root the client expects. The bao decoder verifies every chunk group as it arrives. A malicious node cannot insert a single corrupt byte and still earn — the client rejects the stream before paying for that chunk group. +Delivery uses **bao** — an interleaved verified-stream encoding of the BLAKE3 tree. The paid-delivery payload is always bao bytes: content interleaved with the hash proofs needed to verify each 16 KiB chunk group against the root the client expects. The bao decoder verifies every chunk group as it arrives. A malicious node cannot insert a single corrupt byte and still earn — the client rejects the stream before paying for that chunk group. Because every chunk group carries its own proof, a resumed or ranged fetch verifies **independently** — the decoder aligns the requested offset down to its 16 KiB chunk-group boundary, so there's no need to re-read from the start. Payment meters all transmitted bytes, including the proof bytes: the verification overhead is roughly **0.4%** of blob size, not free metadata. -An origin-cold node must import or build the blob's **outboard** (its BLAKE3 tree) before it can serve verified ranges. Node-to-node cache-miss pulls stay bao-encoded and pipelined end to end. +Where an origin publishes the blob's **outboard** (its BLAKE3 tree) as a sidecar, an origin-cold node streams the origin's bytes straight through to the paying client while teeing them into its own store, verifying each group against the root as it goes. Only when the origin publishes no outboard must the node import the blob and build one first, at a one-time import latency. Node-to-node cache-miss pulls stay bao-encoded and pipelined end to end. -## Hash-to-object-key mapping +## Publishers and namespaces + +A hash answers "what is this blob?". It does not answer "who is responsible for serving it?" — that is a **namespace**, a publisher-owned identifier for a content set and the unit of origin addressing. A delivery request pairs the hash with the namespace its content is published under, and the node routes on it: -Origin backends speak their own namespace: S3 objects are addressed by key, NFS by path, local disk by filename. Blobs are addressed by BLAKE3 hash. The mapping lives in a **content catalog** — a small database (PostgreSQL or SQLite) maintained by the operator: +| Namespace | How the node resolves a miss | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| Named (non-zero) | Routes to that namespace's authorized origins. If none hold the bytes, the fetch fails. | +| Default (zero) | No publisher, no authorized origins — served best-effort from cache or DHT-discovered holders, with no availability guarantee. | -```text -catalog: hash → { s3_bucket, s3_key, size_bytes, content_type } -``` +The namespace is a **routing hint, not a trust anchor**: bytes are verified against the hash independently, so a wrong or hostile namespace can only cause a failed fetch, never corrupt or mis-attributed delivery. Cache-only serving stays permissionless — any staked operator may re-serve bytes it holds, for any hash, under any namespace. Only the origin role is namespace-gated ([takedown](/protocol/takedown#origin-assignment)). + +## Hash-to-object-key mapping -The catalog is not on-chain; it is an operational concern. Origin-backed nodes query it on cache miss to locate the pull URL. +Origin backends speak their own namespace: S3 objects are addressed by key, NFS by path, local disk by filename. Blobs are addressed by BLAKE3 hash. The mapping between them is a private catalog held by the operator and queried on cache miss to locate the bytes. It is not on-chain and not part of the protocol — the protocol only requires that a node deliver the correct bytes for a given hash. ## Why the origin is hidden -Leaking the origin URL would bypass the pay-per-byte economic model — anyone could download directly from S3 and skip the CDN entirely. Redirects point to other nodes, never to an external URL. The origin backend is not addressable from outside the node process. +Leaking the origin URL would bypass the pay-per-byte economic model — anyone could download directly from S3 and skip the CDN entirely. A stream response carries no route hint of any kind, so the backend topology of origin-backed nodes is fully hidden from the network, and the origin backend is not addressable from outside the node process. This also protects origin operators from direct egress cost attacks: an adversary cannot generate S3 traffic by pointing clients at the bucket URL. ## What clients verify vs. trust -| Property | Verified | Trusted | -| ----------------- | ---------------- | -------------------------------------------------------------------------- | -| Byte correctness | BLAKE3 hash tree | — | -| Blob availability | — | Node's signed availability claim (slashable via phantom-announcement path) | -| Origin durability | — | Origin operator's S3/NFS configuration (off-protocol) | +| Property | Verified | Trusted | +| ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------- | +| Byte correctness | BLAKE3 hash tree | — | +| Blob availability | — | Node may decline to serve; the client re-routes and re-pays nothing | +| Rate honesty | — | Quoted rate is binding within the [slashing window](/protocol/slashing#the-slashing-window) (slashable) | +| Origin durability | — | Origin operator's S3/NFS configuration (off-protocol) | diff --git a/docs/protocol/governance.mdx b/docs/protocol/governance.mdx index 6e5baa5..42d2950 100644 --- a/docs/protocol/governance.mdx +++ b/docs/protocol/governance.mdx @@ -5,35 +5,43 @@ description: Voting weight is bytes actually delivered, not TOKEN held — a 4% ## Decision -OpenZeppelin Governor whose voting weight is an operator's **delivered bytes over a trailing window** — not TOKEN balance — with a **4% quorum** and a **2-day timelock**. All governable parameters have hardcoded safety bounds that governance cannot override. A **3-of-5 emergency multisig** can pause contracts, add emergency blacklist entries, and triage disputed slashes and blacklist entries. Its sunset is **split by capability**: the protocol-wide `pause()` brake has a **12-month sunset** enforced via an immutable constructor deadline — governance cannot extend it; prolonging `pause()` past the deadline requires deploying a new contract version and migrating through the standard upgrade path. The narrow unlawful-content-removal power — emergency blacklisting — is **permanent**, discharging an ongoing legal-compliance duty the ~11-day governance takedown path cannot meet. +OpenZeppelin Governor whose voting weight is an operator's **delivered bytes over a trailing window** — not TOKEN balance — with a **4% quorum** and a **2-day timelock**. All governable parameters have hardcoded safety bounds that governance cannot override. A **3-of-5 emergency multisig** can pause contracts, add emergency blacklist entries, decide slash appeals, and suspend a compromised regional governance body. Its sunset is **split by capability**: the protocol-wide `pause()` brake has a **12-month sunset** enforced via an immutable constructor deadline — governance cannot extend it; prolonging `pause()` past the deadline requires deploying a new contract version and migrating through the standard upgrade path. The narrow unlawful-content-removal power — emergency blacklisting — is **permanent**, discharging an ongoing legal-compliance duty the ~10-day governance cycle cannot meet. ## 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). +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. - **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. +- **Declared capacity caps weight; it never grants it.** Bytes above what an operator's declared tier could deliver in a week earn no weight, and an operator that declared zero capacity contributes zero for that week however many bytes it served. This is what stops a lightly-bonded operator inflating its weight by serving traffic to accounts it controls. - **Slashing zeroes out the vote.** A slashed operator's weight drops to zero for the rest of the window, then recovers as the window slides past the slash. -Bytes are read from the on-chain `FeeRouter` settlement counter, so weight is auditable and cannot be claimed without paid delivery. +Bytes are read from the on-chain `FeeRouter` settlement counter, so weight is auditable and cannot be claimed without paid delivery. Voting power is delegable by signed message; the bond itself is not. ### Launch phase -Served-bytes DAO voting only activates once the operator set is large enough to make it safe. Until then — the first 6–12 months post-launch — a **5-of-9 bootstrap multisig** (distinct from the emergency multisig) operates the protocol within the same safety bounds. The bootstrap phase ends, irreversibly, when the active operator count reaches **≥ 30** _and_ total declared capacity reaches **≥ 100 Gbps**; at the transition, governance role authority transfers to the timelock-controlled Governor and the bootstrap multisig cannot be reinstated. +Served-bytes DAO voting only activates once the operator set is large enough to make it safe. Until then — the first 6–12 months post-launch — a **5-of-9 bootstrap multisig** (distinct from the emergency multisig) operates the protocol within the same safety bounds. The multisig never holds governance authority directly: the timelock holds it from deployment onward, so the 2-day delay applies to every parameter change regardless of who requested it. What the bootstrap phase confines is the right to _schedule_. + +The phase ends when the multisig judges the operator set broad and diverse enough — a manual decision, not a threshold that trips on its own. It schedules a single timelock batch granting the Governor the right to propose and cancel, then revoking its own. Executing that batch strips the multisig's ability to schedule anything, including a proposal restoring itself, so the transition is irreversible: only the inheriting DAO could hand the role back, by its own vote. ## Hardcoded safety bounds Even governance cannot set parameters outside these bounds: -| Parameter | Bounds | -| ----------------------------------------- | ----------- | -| Operator revenue share (`FeeRouter` base) | 40% – 90% | -| Burn share (`FeeRouter`) | ≥ 5% | -| Dispute window | 12 h – 72 h | -| Per-operator voting cap | 1% – 25% | -| Unbonding window | 7 – 60 days | +| 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 | + +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. -Bounds are enforced in `require()` checks in the setter functions. A malicious governance proposal that attempts to push the operator revenue share below its floor simply reverts. (Slashing tiers — 5% / 15% / 50% by lifetime offense count — are fixed constants, not governable parameters; see [slashing](/protocol/slashing).) +Bounds are enforced in `require()` checks in the setter functions. A malicious governance proposal that attempts to push the operator revenue share below its floor simply reverts. (The 5% / 15% / 50% slash ladder is a fixed constant in the bond contract, not a governable parameter — see [slashing](/protocol/slashing).) ## Emergency multisig @@ -41,7 +49,8 @@ Bounds are enforced in `require()` checks in the setter functions. A malicious g - **Pause** any contract (`Pausable`) - **Add emergency blacklist entries** (with 14-day auto-expiry or 90-day for CSAM/terrorist categories — see [takedown](/protocol/takedown)) -- **Triage disputed slashes and blacklist entries** — fast-track or reject an appeal within a fixed window, never decide it unilaterally +- **Decide slash appeals** — fast-track or reject an open appeal; the escrow movement itself is performed deterministically by the bond contract +- **Suspend a regional governance body** — governance must ratify or reverse within 14 days, and silence _lapses_ the suspension rather than sustaining it Can **not**: @@ -49,7 +58,11 @@ Can **not**: - Change parameters - Grant roles -The sunset is **split by capability**. The protocol-wide `pause()` brake carries a **12-month sunset** enforced in the multisig's constructor as an immutable deadline — after it, `pause()` reverts on-chain. Emergency **blacklisting does not sunset**: `emergencyAdd()`, `emergencyAddOrigin()`, and `suspendRegionalBody()` are permanent, because unlawful-content removal (CSAM / court-ordered / sanctions) is a time-critical legal duty the ~11-day governance takedown path cannot meet, and touches no economic, treasury, or governance lever. Governance **cannot** modify the `pause()` deadline; retaining `pause()` past the sunset requires deploying a new contract version with a new deadline and migrating to it via the standard upgrade path (timelock + governance vote). This guarantees the `pause()` sunset cannot be silently extended. +The sunset is **split by capability**, and the split is structural rather than a shared clock. The 12-month deadline attaches to **`pause()` alone**: every pausable contract fixes it as an immutable at its own construction, after which `pause()` reverts for every caller — governance included. The pause is the protocol-wide brake and the most centralization-sensitive power the multisig holds, so it is the one that expires. + +The other three capabilities carry **no deadline**. Emergency blacklisting (`emergencyAdd`, `emergencyAddOrigin`) and the regional-body suspension backing it live on the non-pausable blacklist contract, discharging a permanent, time-critical legal duty — CSAM, court-ordered and sanctions removal — that the ~10-day governance cycle cannot meet, and touching no economic, treasury or governance lever. Deciding slash appeals likewise never expires; it moves only already-escrowed funds along deterministic grant/uphold paths. + +Governance **cannot** modify the `pause()` deadline; retaining `pause()` past the sunset requires deploying a new contract version with a new deadline and migrating to it via the standard upgrade path (timelock + governance vote). This guarantees the sunset cannot be silently extended. ## Proposal flow @@ -64,7 +77,7 @@ The combined ~10-day minimum delay (voting delay + 7-day period + 48-hour timelo ## Why 4% quorum -Low enough that realistic participation can reach quorum; high enough that no single operator can unilaterally pass a proposal — and the per-operator weight cap bounds concentration directly. 4% maps to established DAO practice (Compound, Uniswap) and works in conjunction with the hardcoded safety bounds — the bounds mean a bad quorum can still only move parameters within the safe range. +Low enough that realistic participation can reach it — voting weight tracks active service delivery rather than circulating supply, so a bar calibrated against holdings would quickly exceed engaged voting power. Quorum is not the concentration defense and is not sized as one: the per-operator cap is what bounds any single operator. The hardcoded safety bounds do the rest — even a captured vote can only move parameters within the safe range. ## Regional governance bodies diff --git a/docs/protocol/network.mdx b/docs/protocol/network.mdx index 12f5c1d..7cbec08 100644 --- a/docs/protocol/network.mdx +++ b/docs/protocol/network.mdx @@ -1,26 +1,38 @@ --- title: Network topology -description: A flat mesh of staked nodes — gossip on regional and global topics for node discovery, a Kademlia-subset DHT for content, and on-chain registration. +description: A flat mesh of staked nodes — the on-chain registry for node discovery, a Kademlia-subset DHT for content, and probes to confirm availability before paying. --- ## Decision -All staked nodes form a **flat mesh**. Node metadata propagates over a gossip overlay on regional and global topics. Content discovery uses a Kademlia-subset DHT, with broadcast probe fan-out as bootstrap/emergency fallback. +All staked nodes form a **flat mesh** with no routing hierarchy. Node discovery reads the on-chain registry's active set. Content discovery uses a Kademlia-subset DHT, with the on-chain origin directory as the deterministic last-resort fallback. A probe confirms live availability and measures latency before any paid pull. + +## 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. + +## 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. ## Selection -Clients and nodes both rank candidates by a unified score combining advertised price, observed latency, and reputation. Lower is better. The exact weighting is part of the protocol implementation. +Clients and nodes both rank candidates by a unified score combining advertised price, observed latency, and reputation, with a quadratic reputation penalty. Lower is better. ## On-chain registration Nodes register through an on-chain staking registry that binds a network identity to an Ethereum address. Registration requires proof of control of both keys — preventing identity squatting and ensuring every staked node is slashable. Registration also carries acceptance of the DAO-canonical operator terms hash, signed into the registration message and enforced on-chain; the terms hash is governance-swappable, so the accepted version is recorded at registration. Key rotation remains available post-registration. -## Regional topics +## Regions -Nodes subscribe to a region topic plus the global topic. Regional topics reduce gossip noise for latency-sensitive selection: a client in Frankfurt typically prefers DE/NL/AT candidates over US ones, and the regional topic surfaces those preferentially. +A node declares its region — an ISO 3166-1 alpha-2 country code — on-chain at registration, and the registry watcher resolves the active set into a region map. The claim is self-attested and accepted at face value; a requester applies a reputation penalty when observed latency contradicts it, and region-scoped takedown obligations follow it ([takedown](/protocol/takedown)). A stability window after a region change stops a node shedding blacklist scope by flipping regions reactively. -Clients subscribe but do not publish. +Regions inform selection rather than partition the mesh: a client in Frankfurt prefers nearby candidates because they score better on latency, not because it sees a different peer set. ## 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 protocol infrastructure (treasury-funded), not an incentivized network role — paying relays per byte would create a perverse incentive to prevent direct connections. +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. diff --git a/docs/protocol/payments.mdx b/docs/protocol/payments.mdx index a8f7f4f..d92f5be 100644 --- a/docs/protocol/payments.mdx +++ b/docs/protocol/payments.mdx @@ -1,45 +1,74 @@ --- title: Payments -description: Clients pay nodes per MB over off-chain USDC payment channels with on-chain settlement, at market-driven rates inside governance-set bounds. +description: Clients pay nodes per MB with off-chain vouchers backed by one shared on-chain USDC pool, metered by a hash chain and redeemed per node. --- ## Decision -Clients pay nodes **per MB** over off-chain **payment channels** settled on-chain. All channels are denominated in **USDC**, fixed at contract deployment. On a cache miss, nodes pay peers per MB for initial content pulls, then amortize that cost across many client deliveries. Origin-backed nodes set the effective price ceiling (their backend egress costs). Rates are fully market-driven within governance-set bounds. +Clients pay nodes **per MB** with off-chain **vouchers backed by a shared on-chain pool**. All pools are denominated in **USDC**, fixed at contract deployment. On a cache miss, nodes pay peers per MB for content pulls, then amortize that cost across many client deliveries. Origin-backed nodes set the effective price ceiling (their backend egress costs). Rates are otherwise fully market-driven, above a governance-set floor. -## Channel lifecycle +One funded pool backs payments from **many capped signers** to **many nodes**. There is no channel per node and no channel per client: the owner opens a pool once, reuses it, and closes it to reclaim the unspent remainder. + +## Pool lifecycle ```mermaid sequenceDiagram - participant C as Client + participant C as Client (pool owner) participant N as Node - participant CH as PaymentChannel (on-chain) + participant P as PaymentPool (on-chain) - C->>CH: openChannel(provider, deposit) + C->>P: openPool(deposit) + Note over C,N: Capability signed off-chain:
{signer, spending_cap, pool_id, expiry} loop Per stream - C->>N: Cumulative voucher (off-chain, signed, nonce++) + C->>N: Voucher (off-chain, signed, cumulative amount) + C->>N: Preimage per 1 MiB chunk delivered end - N->>CH: closeChannel(latest voucher) - Note over CH: Dispute window — either party
can submit a higher-nonce voucher
via disputeChannel - CH->>CH: settleChannel - CH-->>N: Settle → provider - CH-->>C: Refund remaining deposit + N->>P: redeem(voucher, preimage) + P-->>N: Settle → provider (via FeeRouter) + C->>P: closePool + Note over P: Redemption grace window
lets nodes redeem first + P-->>C: Reclaim unspent remainder ``` -The channel token is fixed (USDC) at deployment, so `openChannel` takes only the provider and deposit. Close is a three-step lifecycle — `closeChannel` (callable by either party) starts the dispute window, `disputeChannel` accepts a higher-nonce voucher during the window, and `settleChannel` distributes funds after it expires. A node may also `withdraw` accrued earnings against the latest voucher while the channel stays open — redeeming a monotonic client-signed claim needs no dispute window. +`openPool` happens **once and off the fetch path**, so it is not latency-critical; `topUp` refills a pool that has been drawn down. A node redeems whenever it likes while the pool is open — redeeming a signed, monotone claim needs no dispute window. The owner closes the pool to reclaim what is left, after a **redemption grace window** (48 hours by default, governable within 48h–72h) during which nodes settle outstanding vouchers. + +## Capabilities -## Cooperative close +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. -When both parties are online and agree on the final balance, they can skip the dispute window entirely. `cooperativeClose(channelId, amount, nonce, bytesDelivered, clientVoucherSig, providerCloseSig)` settles the channel **in a single transaction with no dispute window** — the provider signs an EIP-712 `CooperativeClose` waiver acknowledging the final voucher, and either party can submit it. This is purely additive: it is the fast path when both sides cooperate, and if the node declines or is offline, the client falls straight back to `closeChannel` and the standard dispute window. The client CLI exposes it as `decdn channel coop-close`. +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. + +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. ## Vouchers -Off-chain signed messages bound to a specific channel, carrying a strictly increasing **nonce** (starting at 1) and a **cumulative** amount and byte count. Each new voucher restates the running total owed — both amount and bytes are monotonically non-decreasing — and the highest-nonce voucher is the one that settles. Nodes persist the latest nonce rather than discarding older state, since `disputeChannel` only accepts a strictly higher nonce. Cadence is negotiated per stream. +Off-chain signed messages over `{pool_id, signer, provider, amount, bytes_delivered, chain_root, chunk_price}`. Two properties matter: + +- **Node-addressed.** The voucher names its payee, so one node cannot redeem a voucher meant for another and each node keeps its own independent ordering. +- **Cumulative.** `amount` is a settlement anchor that only moves up. A re-submitted voucher pays zero; there is no nonce and no dispute window. + +## Hash-chain metering + +Between signatures, delivery is metered by a **hash chain** rather than a new signature per interval. The payer commits the chain's root in the voucher and, after verifying each delivered chunk, releases the next **preimage** down the chain. The node hashes that preimage forward until it reaches a value it already trusts. + +Redemption resolves both objects with one formula: + +```text +claimed = amount + chain_index × chunk_price +``` + +A preimage is self-proving — nobody derives a deeper one without the seed — so accepting a tick costs one hash, with no signature, no round trip and nothing to persist before the next chunk ships. Signatures are therefore O(1) per transfer rather than one per interval. + +The payment quantum is the **payment chunk**: `chunk_bytes` = **1 MiB (1,048,576 bytes)**, a protocol constant that nothing negotiates and no message carries. A released preimage has to be worth the same to payer and node, and a fixed constant removes that disagreement instead of negotiating it away. The protocol's **MB is that same 1,048,576 bytes**, so a chunk costs exactly the node's advertised `rate_per_mb` with no rounding at any rate — `chunk_price` is just that quoted rate, signed into the voucher, and a node rejects any _metering_ voucher whose signed `chunk_price` does not match its own quote. A sealed voucher — the cooperative-close shape — meters no chunk and carries `chunk_price = 0`, so the equality check does not apply to it. + +## 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. ## Rate discovery -Each node advertises its own per-MB rate. Governance sets rate bounds (in USDC); a node advertising outside the bounds is ignored by selection. Rates can change between streams. +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). -## Dispute window +## Minimum deposit -A window after `closeChannel` during which either party can submit a later (higher-nonce) voucher via `disputeChannel` if a stale close was filed. Default 48 hours, governable within 12h–72h. +`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. diff --git a/docs/protocol/privacy.mdx b/docs/protocol/privacy.mdx index d5c26e8..d68995a 100644 --- a/docs/protocol/privacy.mdx +++ b/docs/protocol/privacy.mdx @@ -1,42 +1,44 @@ --- title: Privacy analysis -description: An accountability-first privacy design — a four-tier adversary model, an inventory of privacy surfaces, and the four specified mitigations. +description: An accountability-first privacy design — a four-tier adversary model, an inventory of privacy surfaces, and the two mitigations it commits to. --- ## Decision -Privacy is treated as an **accountability-first** design: probes are public, on-chain channels enable disputes, gossip enables discovery. Most privacy surfaces are accepted as inherent. Four concrete mitigations are specified below; deeper techniques (dummy probes, channel mixing) are future work. +Privacy is treated as an **accountability-first** design: probes are public, on-chain settlement is what makes payment auditable, and the registry is what makes nodes discoverable and slashable. Most privacy surfaces are accepted as inherent. Two are mitigated; deeper techniques are assessed and not adopted. ## Adversary model (four tiers) -| Tier | Example adversary | What they can observe | -| --------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------- | -| **Passive observer** | On-path network middlebox | QUIC metadata (IPs, SNI unless ECH is deployed), timing, volume | -| **Active participant** | Malicious node or client | Probes, gossip activity, payment channel open/close | -| **Infrastructure operator** | L2 RPC provider, relay host | On-chain events at read time; relay endpoints see encrypted QUIC flows | -| **Compromised endpoint** | Malicious origin or content-provider infrastructure | Plaintext bytes, client subscription identity | +Each tier subsumes the one above it. + +| Tier | Example adversary | What they can observe | +| --------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| **Passive observer** | ISP, blockchain analytics firm | QUIC connection metadata (IP pairs, timing, volume), the negotiated ALPN, and everything on-chain | +| **Active participant** | Competing CDN, curious node operator, researcher | Plus: probes it receives, stream requests, and the probe responses it collects | +| **Infrastructure operator** | RPC provider, relay host | Plus: which registry and blacklist queries a client makes, and IP pairs on relayed connections | +| **Compromised endpoint** | Device theft, malware, warrant | Plus: memory and disk on that one machine | ## Privacy surfaces The protocol inventories its distinct privacy surfaces with explicit dispositions (accept or mitigate). Highlights: -- **On-chain settlement volume leakage.** Anyone can read channel open/close amounts on-chain. Accepted: disputes require public channel state. -- **Signed protocol messages as non-repudiable content commitments.** A node that signs an availability response has provably committed to serving that hash. Accepted: the same signature is what enables on-chain accountability. -- **Probe traffic.** Who probes what is visible to the probed node. Accepted: selection depends on probing; fully private probing would require PIR at O(log N) blow-up. +- **On-chain settlement volume leakage.** Anyone can read pool deposits and per-node redemptions on-chain. Accepted: auditable settlement is what makes the payment layer trustless. +- **Signed protocol messages as a content inventory.** A signed availability response is non-repudiable proof that the node held (or did not hold) a specific hash at a specific time, and accumulated signatures build a verifiable inventory. Accepted: the same signature is what enables on-chain accountability. +- **Probe traffic.** Who probes what is visible to every candidate probed, not just the one selected. Accepted: the delivering node must learn the hash regardless, so hiding the request from the others adds bandwidth without changing the underlying property. +- **Registry enumeration.** Anyone can read the active set and each node's self-reported region and addresses. Accepted: an enumerable, slashable operator set is the point. ## Mitigations -1. **Client NodeId rotation.** Clients rotate NodeIds on session boundaries to break linkability across subscription sessions. -2. **Bounded availability hints.** Probe responses include only a small number of additional cached hashes, lowering the precision of per-node content fingerprinting. -3. **Client key encryption via platform keychain.** Clients store Ethereum keys in macOS Keychain, Windows Credential Manager, or Gnome Keyring — not plaintext files. -4. **Multi-source RPC.** Clients and nodes use multiple independent RPC providers to reduce single-vendor deanonymization surface. +1. **Client key storage.** The design keeps client keys in OS-level secure storage — the macOS Keychain, Windows Credential Manager, or the Linux Secret Service — rather than plaintext files on disk. +2. **Operational RPC guidance.** Clients and nodes query through multiple independent RPC providers, or a self-hosted node, so no single vendor sees the whole query pattern. -## Future work +## Assessed and not adopted -- **Dummy probes.** Inject cover-traffic probes to obscure real content interest. Unresolved cost/benefit. -- **Payment channel mixing.** Route settlements through a mixing contract. Regulatory complexity. +- **Client identity rotation.** Rotating the transport identity between sessions is a supported capability, but it is not a privacy commitment: every request carries the pool identifier, and a serving node must resolve the signer's address to be paid at all. That stable payment identity is what an observer correlates on, regardless of node count — so rotation obscures only non-serving probers, whose view is already accepted above. +- **Dummy probes.** Cover-traffic probes hide requests only from nodes that were never going to deliver — the delivering node must learn the hash regardless — 3–5× the probe traffic for marginal benefit. +- **Payment pool mixing.** Breaking the on-chain payer↔provider link needs regulatory analysis before any design work. Pools are also long-lived and amortized across many sessions, so they reveal less than per-transfer settlement would. ## Scope clarifications -- **Content plaintext** is a content-provider concern. If sensitivity requires it, the content provider encrypts before publishing. -- **Client IP addresses** are visible to peers. Tor-style relaying is out of scope — adding a layer of indirection conflicts with the pay-per-byte accountability model. +- **Content plaintext** is a content-provider concern, and application-level privacy is out of scope. If sensitivity requires it, the content provider encrypts before publishing. +- **Client IP addresses** are visible to peers, and to a relay host on any relayed connection. Network-level anonymity is not a protocol goal. diff --git a/docs/protocol/reputation.mdx b/docs/protocol/reputation.mdx index b872012..21cac77 100644 --- a/docs/protocol/reputation.mdx +++ b/docs/protocol/reputation.mdx @@ -1,35 +1,50 @@ --- title: Reputation -description: Nodes are scored 0.0–1.0 from local delivery outcomes plus weighted gossip from staked peers, with per-report clamps, decay, and a cold-start bonus. +description: Each node scores its peers 0.0–1.0 from its own delivery outcomes alone — an EWMA over speed, correctness and reachability, decaying toward neutral. --- ## Decision -Nodes are ranked by a **reputation score** in `[0.0, 1.0]` derived from a combination of direct local observation and signed gossip reports from staked nodes. Local experience dominates; gossip sharpens the picture for nodes you have not yet interacted with. +Reputation is **local-only**. Each node scores its peers from its own direct delivery observations and nothing else: no propagation, no cross-node aggregation, no network-wide score, no on-chain reputation state. A node's ranking of a peer reflects only interactions it witnessed itself — the same tit-for-tat principle BitTorrent uses for peer selection, which needs no global reputation to work at scale. -Only staked nodes may submit gossip reports. Clients contribute to local scores only. +Reputation is **not** the Sybil or corruption defense — stake, the capacity bond, mandatory progressive BLAKE3 verification and no-payment-on-failed-delivery are. It is not an eligibility gate, a governance input, or a component of settlement or vote weight. It only tunes each requester's selection probability among otherwise-eligible nodes. -## Reporter weighting +## Scoring -A report's weight is a function of the reporter's track record and counterparty diversity, computed against on-chain settlement history. A node that has only ever settled with itself and a few colluders is heavily down-weighted; a node that has settled fairly with many counterparties is trusted more. Reporter weight is bounded so no single party dominates. +Scores live in `[0.0, 1.0]`. A peer a node has never used is **unscored and treated as neutral (0.5)**, so it stays fully selectable — traffic keeps exploring rather than converging on incumbents. -## Clamping and cold-start +After each interaction the node folds the outcome in with an EWMA (α = 0.1): -- **Per-report clamp.** A single report can move a target's score by at most a bounded amount, preventing flash-crashes from coordinated negatives. -- **Decay toward neutral.** In the absence of fresh reports, scores drift toward a neutral value over time. -- **Cold-start bootstrap.** New nodes get a small initial reputation bonus so they can earn their first traffic. The bootstrap is one-time per operator address — re-staking under a new identity does not replay it. +```text +local_score = ewma(local_score, interaction_score, alpha = 0.1) +interaction_score = 0.4 × speed + 0.4 × correctness + 0.2 × reachability +``` -## What triggers reports +| Signal | Range | Weight | +| ------------------------------------------ | ------------ | ------ | +| Delivery speed (bytes/sec vs. a reference) | 0.0–1.0, log | 40% | +| Data correctness (BLAKE3 verified) | 0.0 or 1.0 | 40% | +| Connection success (reachable?) | 0.0 or 1.0 | 20% | -Interaction outcomes generate reports. Examples: +Speed is normalized on a log curve against a node-local reference throughput (default 1 GiB/s), reaching its maximum at that reference. Below it the curve keeps climbing rather than **flattening at a low baseline** — a materially faster peer earns a materially better score instead of tying with every other fast peer. -| Event | Local score change | Gossip report? | -| -------------------------------------------- | ------------------ | ------------------------------------------------- | -| Clean delivery, valid voucher accepted | Small positive | Optional | -| Hash mismatch mid-stream | Large negative | Yes | -| Probe claimed availability but stream failed | Negative | Yes (candidate for phantom-announcement slashing) | -| Charged rate ≠ advertised rate | Negative | Yes (candidate for rate-manipulation slashing) | +A single interaction moves a peer's score by at most **±0.05**, so one bad exchange cannot destroy a good peer's standing. -## Why not purely on-chain? +## Why correctness is only one signal among several -On-chain reputation scales poorly — thousands of report transactions per hour would be cost-prohibitive. Reports stay in gossip and only escalate to on-chain slashing when the evidentiary bar is crossed. Reporter weighting still anchors to on-chain settlement history, so it remains auditable. +Reputation measures service quality, not honesty. Corruption is absorbed at the wire: progressive BLAKE3 verification means a corrupt window yields no voucher, so the node ships garbage unpaid. The reputation hit compounds on top — a corruption event zeros the correctness component, moves the EWMA down, and degrades selection through a quadratic reputation penalty. Lost revenue plus lost traffic make sustained corruption irrational without an on-chain path. + +## Decay toward neutral + +Without fresh data a score decays back to 0.5, applied closed-form at read time from the last update: + +```text +score_new = 0.5 + (score_old − 0.5) × 0.5 ^ (elapsed / half_life) +``` + +`half_life` is node-local, default 3 days. Each half-life halves the distance to neutral, so a peer that stops being used drifts back to unopinionated rather than holding a stale high or low score. + +## What does not move a score + +- **Requester-side aborts.** A pull abandoned because throughput fell below the requester's floor produces no interaction outcome and never updates the EWMA. The slowness may come from the link, from congestion, or from the requester's own consumption, and a throughput signal is spoofable — so it is not evidence about the peer. The requester still stops using that peer for that blob, as a local, reputation-neutral suppression. +- **Anything a peer reported.** Scores never travel between nodes, so there are no third-party reports to weight, replay, or coordinate. diff --git a/docs/protocol/slashing.mdx b/docs/protocol/slashing.mdx index 34349a9..9a22595 100644 --- a/docs/protocol/slashing.mdx +++ b/docs/protocol/slashing.mdx @@ -1,17 +1,33 @@ --- title: Slashing & on-chain verification -description: Three slashable offenses — phantom announcement, rate manipulation, blacklist violation — adjudicated on-chain, with a 30-day operator appeal. +description: Two slashable offenses — rate manipulation and blacklist violation — adjudicated on-chain via commit–reveal, with a 30-day operator appeal. --- ## Decision -Three slashable offenses with concrete on-chain evidence paths: +Two slashable offenses, each resting on cryptographically dispositive signed evidence: -1. **Phantom announcement** — claimed availability, failed to deliver. -2. **Rate manipulation** — charged a rate different from the advertised rate. -3. **Blacklist violation** — served a blacklisted hash after the compliance window. +1. **Rate manipulation** — charged a rate **higher** than the rate quoted in a signed probe response within the slashing window. (Charging less than quoted is not an offense.) +2. **Blacklist violation** — served a blacklisted hash after the compliance window. -A unified on-chain judge adjudicates all three synchronously and triggers the bond reduction immediately upon verification — there is no in-protocol counter-evidence window, because each offense rests on cryptographically dispositive signed evidence. The reduced bond is then held in escrow pending a post-slash appeal (below), not distributed on the spot. +Every probe and stream response carries a mandatory signature over its security-relevant fields, produced with the node's Ethereum key, so any protocol message is portable evidence verifiable on-chain. A unified on-chain judge adjudicates both offenses synchronously and triggers the bond reduction immediately upon verification — there is no in-protocol counter-evidence window. The reduced bond is then held in escrow pending a post-slash appeal (below), not distributed on the spot. + +## What is not slashable + +- **Corrupted delivery.** Absorbed at the wire: the payer verifies each chunk group as it arrives and pays only behind verification — it withholds the preimage that would meter a corrupt chunk, and signs no voucher covering it. The node ships garbage unpaid, and its local reputation drops. There is no on-chain path. +- **Refusing to serve.** A node may decline any request — for price, capacity, or policy reasons. The client re-routes to another provider and pays only a latency cost. + +## Commit–reveal challenges + +Submitting evidence is a two-phase flow: a `commitChallenge` registers an opaque commitment that moves no funds, and the reveal resolves synchronously once the commitment matures. + +The phases exist because the reveal must disclose the full evidence — the signed responses themselves — in its calldata. Without a prior commitment, a mempool watcher could copy a pending honest challenge, resubmit it under its own address, and capture the challenger reward. The bond is never what's at stake here — it is returned on a successful challenge and never transferred at all on a failed one — so the reward is the whole exposure, and it is also the entire incentive for off-path witnesses. Leaving it extractable would hollow out enforcement. + +## The slashing window + +A node's last probe-quoted rate is binding for **30 seconds**. Any stream opened inside that window must be served at or below the quoted rate; a legitimate rate change waits the window out before serving at the new price. Two signed messages from the same node disagreeing about its own rate inside 30 seconds are non-repudiable, which is what makes the offense adjudicable without a counter-evidence step. + +This is distinct from **evidence staleness** below: the slashing window bounds when an offense can be _committed_, evidence age bounds how long it can be _submitted_. ## Evidence staleness @@ -19,8 +35,8 @@ Evidence has a maximum age, set strictly below the unbonding period — so an ac ## Escrow and appeals -The bond reduction is immediate, but the slashed TOKEN sits in `CapacityBond` escrow rather than being paid out at slash time. The slashed **operator** — and only the operator — may file an **operational-failure appeal** within **30 days** (e.g. a genuine network outage, NTP drift, or hosting incident prevented compliance), posting a TOKEN appeal bond. The emergency multisig fast-tracks or rejects within a fixed window; the Governor then grants or upholds. A **granted** appeal refunds the full escrowed bond to the operator; a **failed** appeal distributes the escrow like an unappealed slash. The lifetime offense counter is unchanged either way — an appeal restitutes capital, not standing. +The bond reduction is immediate, but the slashed TOKEN sits in escrow rather than being paid out at slash time. The slashed **operator** — and only the operator — may file an **operational-failure appeal** within **30 days** (e.g. a genuine network outage, NTP drift, or hosting incident prevented compliance), posting a TOKEN appeal bond. The emergency multisig fast-tracks or rejects within a fixed window; the Governor then grants or upholds. A **granted** appeal refunds the full escrowed bond to the operator; a **failed** appeal burns the appeal bond and distributes the escrow like an unappealed slash. The lifetime offense counter is unchanged either way — an appeal restitutes capital, not standing. ## Challenger incentives -Submitting evidence requires a **100 TOKEN** bond. A challenge that fails on-chain verification simply reverts — the challenger only loses gas, and the bond is not transferred. A challenge that passes always slashes the node and the bond is returned. At finality (≤ 30 days with no appeal, later if one is filed) the escrowed bond is split **50% to the challenger / 50% burned**, unless the operator's appeal is granted, in which case the operator is refunded and no challenger reward is paid. +Submitting evidence requires a challenge bond — **100 TOKEN** by default, governance-set within a 1–1,000 TOKEN band — transferred at the reveal. A reveal that fails on-chain verification simply reverts — the challenger only loses gas, and the bond is not transferred. A challenge that passes always slashes the node and the bond is returned. At finality (≤ 30 days with no appeal, later if one is filed) the escrowed bond is split **50% to the challenger / 50% burned**, unless the operator's appeal is granted, in which case the operator is refunded and no challenger reward is paid. diff --git a/docs/protocol/takedown.mdx b/docs/protocol/takedown.mdx index 488232c..d5ac716 100644 --- a/docs/protocol/takedown.mdx +++ b/docs/protocol/takedown.mdx @@ -1,11 +1,14 @@ --- title: Content takedown -description: An on-chain hash blacklist with global and regional entries, a 24-hour compliance window for governance takedowns, and an immediate emergency path. +description: Symmetric on-chain authority over origins — a hash blacklist with global, regional and emergency entries, plus publisher-seated origin assignment. --- ## Decision -A **`ContentBlacklist`** contract supports **global** (network-wide) and **regional** (jurisdiction-scoped) takedown. Standard governance entries have a **24-hour compliance window**; the emergency multisig path takes effect **immediately** with a **2-hour slash window**. +Content governance over origins has two symmetric authorities, both DAO-controlled: + +- **Negative — `ContentBlacklist`.** Removes hashes and operators through a **global** (network-wide) path and a **regional** (jurisdiction-scoped) path. Standard governance entries carry a **24-hour compliance window**; the emergency multisig path takes effect **immediately** with a **2-hour slash grace**. +- **Positive — `OriginAssignment`.** Authorizes specific operators to act as origins for specific namespaces ([content addressing](/protocol/content-addressing#publishers-and-namespaces)). The emergency takedown power is a **permanent** capability — unlike the multisig's protocol-wide `pause()`, which [sunsets at 12 months](/protocol/governance#emergency-multisig), emergency blacklisting does not sunset, since unlawful-content removal is an ongoing legal-compliance duty. (Individual emergency _entries_ still auto-expire unless ratified — see [Entry types](#entry-types) below.) @@ -17,7 +20,7 @@ The blacklist must be: - **Atomic per-node.** Every node polls the same source of truth. No partial propagation. - **Auditable.** Anyone can see what has been blacklisted and when. -Gossip-only blacklists fail all three. On-chain entries with governance gating meet them. +Off-chain distribution fails all three. On-chain entries with governance gating meet them. ## Entry types @@ -26,28 +29,45 @@ Gossip-only blacklists fail all three. On-chain entries with governance gating m | Standard governance | Governor | 24 h after vote execution (compliance window) | None (governance can remove) | | Regional | Regional governance body | 24 h | None | | Emergency | 3-of-5 multisig | Immediately (with 2 h slash grace) | 14 days | -| Emergency (CSAM / terrorist) | 3-of-5 multisig | Immediately | 90 days | +| Emergency (CSAM / terrorist) | 3-of-5 multisig | Immediately (with 2 h slash grace) | 90 days | + +Emergency entries **auto-expire unless ratified by governance** before the deadline — preventing the multisig from maintaining a permanent de-facto blacklist without oversight. Expiry needs no transaction: the entry simply stops being enforceable, and anyone may then clean it out of storage permissionlessly. -Emergency entries **auto-expire unless ratified by governance** before the deadline — preventing the multisig from maintaining a permanent de-facto blacklist without oversight. +The emergency path is deliberately one-way with respect to governance — it can always make enforcement stricter, never looser. Both compliance windows are governable within one set of hardcoded bounds: minimum 1 hour, maximum 7 days. The floor is what keeps the emergency path honest — governance cannot compress the window below a single poll cycle and slash nodes for entries they had no opportunity to learn about. ## Why the 2-hour slash grace -The emergency path takes effect immediately in terms of "don't serve this", but the first 2 hours cannot result in a slash. This absorbs gossip propagation lag and clock skew — a node that genuinely hasn't seen the entry yet is given a realistic reaction window before liability attaches. +The emergency path takes effect immediately in terms of "don't serve this", but the first 2 hours cannot result in a slash. This absorbs blacklist-poll lag and clock skew — a node that genuinely hasn't seen the entry yet is given a realistic reaction window before liability attaches. ## Regional scoping -A **regional** entry applies only to nodes that advertise a matching region. The right to add regional entries is granted to regional governance bodies on a per-region basis. +A **regional** entry applies only to nodes that advertise a matching region. The right to add regional entries is granted to regional governance bodies on a per-region basis; the emergency multisig can suspend a compromised body immediately. Governance must then ratify or reverse within 14 days, and **silence lapses the suspension** — the body resumes writing, exactly as an unratified emergency entry expires. Neither is a standing act of governance, and sustaining one on silence alone would let the multisig disable a jurisdiction permanently with no vote ever taken. Suspension also bounds only a body's _future_ authority: entries it already issued stay live. + +Scope follows the **operator's** declared region — never the requester's. A node serving a globally-blacklisted hash is always slashable. A node declaring `DE` is slashable for a `DE`-regional entry; a node declaring `US` is not slashable for a hash blacklisted only by the EU body, regardless of where its clients are — the requester's location is not an input. A node cannot shed exposure by flipping regions after the fact either: slash eligibility is evaluated against the region in force when the bytes were served. + +## Sub-day removal orders + +Some statutory regimes bind the operator that receives a removal order to a sub-day deadline — the EU Terrorist Content Online Regulation's one-hour clock is the tightest. Such an order is discharged at the operator level, on the [local denylist](#local-denylist): it takes effect on the next reload, needs no governance round-trip, and binds exactly what the order binds — the recipient's own serving. Network-wide removal on that clock goes through the emergency multisig. -A node serving a globally-blacklisted hash is always slashable. A node in `region: DE` serving a `DE`-regional entry is slashable; that same node serving the entry via a US client is a gray area — current guidance is that operators adopt the strictest jurisdiction their node's region subjects them to. +## Origin assignment + +The blacklist is the DAO's authority to _remove_ an origin. `OriginAssignment` is the symmetric authority to _grant_ one, and it separates two planes: + +- **Vetting** is governance's job, done once per publisher wallet under a swappable vetting policy. +- **Seating** is the publisher's job. A vetted publisher picks its own origin operators from the bonded set, per namespace, instantly and with no further governance action. Multiple operators may serve one namespace, so redundancy is a namespace-level property. + +Without positive authority, origin assignment would be entirely off-protocol: no Sybil resistance on origin claims, no enforced redundancy, and no way to pre-authorize rather than punish after the fact. Cache-only serving remains permissionless throughout — only the origin role is gated here. ## Origin blacklisting -Re-uploading blacklisted content under a fresh hash (trivial re-encoding) is defeated by **origin blacklisting** — the blacklist contract can blacklist an **operator address**. Re-uploading requires fresh stake under a new identity. This raises the cost of hash evasion from "re-encode" to "spin up a new operator, restake, re-register". +Re-uploading blacklisted content under a fresh hash (trivial re-encoding) is defeated by **origin blacklisting** — the blacklist contract can blacklist an **operator address**. A blacklisted operator is ejected from the registry, drops out of every namespace's authorized origin set, has its remaining bond enter forced unbonding, and cannot re-register under the same address while the entry stands. It cannot self-reinstate by re-bonding: lifting the ban is a governance action. + +Ejection is not slashing — the bond is returned after unbonding absent a separate offense. Serving a blacklisted hash is the slashable act; repeatedly sourcing blacklisted content is what gets an operator ejected. Together they raise the cost of hash evasion from "re-encode" to "spin up a new operator, restake, re-register". ## Local denylist -Each node maintains a **local denylist** for direct legal notices (e.g., a DMCA takedown served to one operator). Local denylists don't propagate and don't trigger slashes elsewhere — they're a per-operator compliance tool. +Each node maintains a **local denylist** for direct legal notices (e.g., a DMCA takedown served to one operator). Local denylists don't propagate and don't trigger slashes elsewhere — they're a per-operator compliance tool, and the fastest removal path the protocol offers. ## Slash schedule -Blacklist violations use an escalating schedule: **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 minimum is auto-ejected.