Skip to main content

Decision

Four ALPN-negotiated QUIC protocols plus iroh-gossip. cdn/client/v1 covers all paid delivery — client→node and node→node.

Core CDN protocols

ProtocolPurpose
cdn/probe/v1Parallel latency + availability check before node selection
cdn/client/v1Paid delivery: client→node and node→node (cache miss)
cdn/dht/v1Kademlia-subset content discovery (content discovery)
cdn/watchtower/v1Channel-dispute monitoring, voucher registration (watchtower)
iroh-gossipNode metadata broadcast (NodeAnnounce), rate changes (RateChange), watchtower discovery (WatchtowerAnnounce)

Companion protocol (app server — not a CDN participant)

ProtocolPurpose
cdn/keys/v1Epoch key delivery, play requests, offline leases — used by the app server
The app server shares iroh QUIC transport but does not gossip, probe, or stake.

Gossip topics

TopicMessages
cdn/global/v1NodeAnnounce, RateChange, WatchtowerAnnounce
cdn/region/{cc}/v1NodeAnnounce
cdn/reputation/v1Reputation reports (reputation)

Core message types

ProbeRequest / ProbeResponse

ProbeRequest { hash: BlobHash }
ProbeResponse {
    has_blob: bool,
    token_rates: Vec<TokenRate>,
    load_hint: LoadHint,
    popular_hashes: [BlobHash; N],  // capped (see privacy)
    wire_sig: Ed25519Sig,
    slash_sig: Option<Secp256k1Sig>,  // required for on-chain evidence
}

StreamRequest / StreamResponse

StreamRequest {
    hash: BlobHash,
    offset_bytes: u64,          // for resume
    payment_token: Address,
    voucher_cadence_bytes: u64, // negotiable (default 1 MiB)
}
StreamResponse {
    status: StreamStatus,       // Streaming | Redirect | Error
    redirect: Option<NodeId>,   // never an external URL
    max_bytes_per_voucher: u64,
}

Voucher (client→node, off-chain)

Voucher {
    channel_id: H256,
    amount: U256,
    nonce: u64,
    token: Address,
    signature: Eip712Sig,
}

Origin URL hiding

redirect in StreamResponse always points to a NodeId, never an external URL. The origin backend is never revealed. This is enforced at the schema level — the type is Option<NodeId>, not Option<String>.

Probe-triggered eviction hold

When a node signs has_blob: true in a ProbeResponse, the blob is temporarily exempt from eviction via the eviction hold (probe_hold_duration). This prevents false phantom-announcement slashing when cache pressure would otherwise evict a blob between probe and subsequent stream request. Nodes expose decdn_probe_hold_slots_used / decdn_probe_hold_slots_max (observability) and should alert when saturation exceeds ~80%.

Error semantics

Stream errors use typed variants: BlobTooLarge, RateExceeded, ChannelInsufficient, Blacklisted, Unavailable. Clients should back off exponentially on Unavailable and re-probe on RateExceeded. max_blob_size is per-node operational policy (recommended default 10 GB) — the protocol does not mandate a single value. Source ADR: 005-protocol.md