Bonded operators cache and serve BLAKE3-addressed content over iroh QUIC. Clients pay for every megabyte in USDC and verify every byte against its content hash, so any node can serve any blob without being trusted.
Website Β· Docs Β· Network stats Β· Discord Β· X
Download Qwen2.5 0.5B Instruct, a 491 MB GGUF that runs in llama.cpp or ollama, from the testnet. It takes about a minute on a 100 Mbps line. On macOS or Linux:
curl -fsSL https://up.decdn.org/decdn.sh | sh -s -- get 'decdn:d0a3eac609db6f49de12213574a302e42afd45f15b2cd89aa46b8a3e91f74d13/?ns=1'On Windows, in PowerShell:
& ([scriptblock]::Create((irm https://up.decdn.org/decdn.ps1))) get 'decdn:d0a3eac609db6f49de12213574a302e42afd45f15b2cd89aa46b8a3e91f74d13/?ns=1'The file's SHA-256 is 74a4da8c9fdbcd15bd1f6d01d621410d31c6fc00986f5eb687824e7b93d7a9db, the
one Hugging Face publishes. For a
bigger pull, Mistral 7B Instruct v0.3 is about 29 GB; either line takes its URI in place of
the one above:
curl -fsSL https://up.decdn.org/decdn.sh | sh -s -- get 'decdn:9194000d7b356650e6924a7746ec4afb0b705838b65913c0e46cba6b15af69e5/?ns=1'The line installs decdn and asks you to pass one check in the browser (it opens the
link, or prints it). Then it downloads the bundle into its own ./<hash>/ directory and
verifies every byte on the way in. It refuses to overwrite a file it did not write itself;
add --overwrite after the URI to replace one. Run it again after an interruption and it
resumes. A sponsor pays for the transfer, so you need no wallet, keys, or account.
Later, decdn self-update moves the installed binary to the latest signed release.
The catalogue on decdn.org lists more seeded content. To pay for your own users' downloads the same way, run sponsord, the sponsorship daemon and onramp that ship with every release.
- Verified bytes, untrusted nodes. Content is BLAKE3-addressed and the hash is a Merkle root, so any byte range verifies on its own, which makes resume and multi-source fetch safe (ADR 002, ADR 038).
- Pay per megabyte. A client funds one shared
PaymentPoolin USDC and pays each node with cumulative off-chain vouchers (ADR 003). - Anyone can serve. Operators bond TOKEN against their declared bandwidth, register on-chain, then cache and serve content (ADR 001, ADR 026).
- Multi-source fetch. A large fetch spreads disjoint byte ranges across several holders and reassigns ranges as sources finish or stall (ADR 039).
- Private origins. S3, R2, B2, HTTP, and filesystem backends are per-node config. No origin URL is ever exposed (ADR 002).
- Enforced on-chain. Every quote and stream response carries a signature that serves as on-chain slash evidence, a slashed operator has 30 days to appeal, and governance blacklists content hashes (ADR 014, ADR 028, ADR 011).
- Governed by service. No TOKEN balance carries a vote. Vote weight is trailing-window served bytes (ADR 036).
Public testnet on Arbitrum Sepolia; production targets Arbitrum One. The contracts ship at full production shape, with governance-tunable economics from day one (ADR 016). The implementation is Rust. Signed release archives and container images ship with each release, and crates publish to crates.io.
Wire, ABI, config, and storage formats change without compatibility shims until the mainnet deployment. Many ADRs are still marked Draft, so the specification can change too.
cargo binstall --locked decdn-cli decdn-node # needs cargo-binstall; installs the release archive
cargo install --locked decdn-cli decdn-node # or build from source
docker pull ghcr.io/decdn/decdn-node # daemon + CLI; mirrored to docker.io/decdn/decdn-node
docker pull ghcr.io/decdn/decdn # CLI only; mirrored to docker.io/decdn/decdn
docker pull ghcr.io/decdn/sponsord # sponsorship daemon, for publishers who pay for their users' downloads
docker pull ghcr.io/decdn/sponsord-onramp # its public onramp; both mirrored to docker.io/decdn/β¦Each GitHub release also carries
decdn-${VERSION}-${TARGET} and decdn-node-${VERSION}-${TARGET} archives (.tar.gz,
or .zip on Windows), and sponsord-β¦ and sponsord-onramp-β¦ archives for Linux.
SECURITY.md explains how to verify each channel.
A decdn installed from an archive or by the one-liner updates itself:
decdn self-update fetches the latest release, verifies its SHA256SUMS.minisig
against a maintainer key the binary embeds, and replaces the binary in place. A binary in a
cargo bin directory (cargo install, cargo binstall), or one in a directory you cannot
write (the container image, a package), is refused with the command to run instead. Once a day, after
a command succeeds with the terminal as its output, decdn prints one line when a
newer release exists; [self_update] check = false in node.toml or
DECDN_SELF_UPDATE_CHECK=0 turns that off. decdn-node never updates itself.
Running the container images
The decdn-node image runs the daemon and also carries the CLI, so
docker exec <container> decdn node status reaches the daemon's local admin
port. The decdn image runs the CLI. Mount your key directory and a working
directory:
docker run --rm -it -v ~/.decdn:/home/decdn/.decdn -v "$PWD":/work -w /work \
ghcr.io/decdn/decdn get decdn:<hash> -o blob.binBoth images run as uid 1000. If your user has another uid, add
--user "$(id -u):$(id -g)" -e HOME=/home/decdn. Without a TTY, mount a file
that holds the keystore password and point DECDN_KEYSTORE_PASSWORD_FILE at it.
A maintainer signs the SHA256SUMS manifest that covers the release archives, and each
container image's digest file and SBOM. crates.io artifacts carry no maintainer signature.
Build from source
You need the Rust toolchain that rust-toolchain.toml pins
(rustup installs it automatically), a C compiler, and make.
git clone --recurse-submodules https://github.com/decdn/decdn.git
cd decdn
cargo build --release -p decdn-cli -p decdn-node
# Binaries: target/release/decdn and target/release/decdn-nodeCONTRIBUTING.md lists the full development prerequisites, including Foundry for the contracts.
A client uses its own keystore under ~/.decdn/client, separate from any node identity
on the same host. Fund the address that key-gen prints with Arbitrum Sepolia ETH and
USDC.
decdn config init --client --output ~/.decdn/client.toml
decdn key-gen --output-dir ~/.decdn/client
decdn --config ~/.decdn/client.toml quote decdn:<blake3-hash>
decdn --config ~/.decdn/client.toml get decdn:<blake3-hash> -o ./blob.bin --receipt ./blob.receipt.json
decdn receipt verify ./blob.receipt.jsonquote prices the pull from the rates the nodes sign in their probe answers, and pays
nothing for a blob; a bundle with no cached manifest costs one small paid pull for it,
from your open pool. With no open pool, quote stops before it opens one: add
--open-pool to let it, which escrows the working deposit (10 USDC by default) and pays
gas, or fund a pool first with decdn pool open. With or without the flag, an open pool
that runs low gets a top-up, which also escrows USDC and pays gas.
get prints the same estimate on one line before it starts. It ends with a receipt: the
bytes it verified, the nodes that served them, and the USDC it paid. --receipt writes
the nodes' signed answers and your vouchers to a file that decdn receipt verify checks
offline and decdn receipt settle updates once the nodes redeem
(appendix).
get pays each node it uses from a pool you own. It reuses your existing pool or opens one with a 10 USDC working deposit, and it tops the
pool up on-chain without prompting when the balance runs low. Fund the wallet with only
what you are willing to spend.
decdn: URIs, node selection, and overwrite rules
A decdn: URI names a blob, a whole bundle, or one file of a bundle
(appendix):
| URI | get pulls |
-o is |
Without -o |
|---|---|---|---|
decdn:<hash> |
the blob | a file, or - for stdout |
./<hash> |
decdn:<hash>/ |
every file of the bundle whose manifest has this hash | a directory | ./<hash>/ |
decdn:<hash>/a/b.txt |
that one file of the bundle | a directory, or - for stdout |
./b.txt |
any of these with ?ns=<id> |
the same, routed to a namespace's origins | the same | the same |
Without -o, a bundle or entry pull refuses to replace a file unless an earlier pull wrote it
there and it has not changed since. That earlier pull must be of the same entry, or have
written the same bytes. It also refuses a hidden path β one with a .-prefixed component β
outright; name the directory with -o <dir> to pull such paths. A new version of a bundle
has a new hash, so it lands in its own directory; to update the old directory in place, pass
-o <the old bundle's directory>: the pull replaces the files that changed and skips the
rest. --overwrite fetches every file again.
get probes bonded nodes and picks one that holds the blob. When none does, it tries
bonded non-holders nearest first. Each fills the miss from its own origin, or from a peer
if it has pull-through on. A node with neither answers not-found, and get moves to
the next. One pool pays every provider, so there is no per-provider setup. Use
decdn pool {β¦} to inspect, close, or reclaim a pool.
You need Arbitrum Sepolia ETH for gas, TOKEN for the bond, and a keystore password source for the headless daemon. The operator guide covers testnet funds, the bond (ADR 026 Β§ Capacity-bond curve), and the full quickstart.
decdn config init # writes ~/.decdn/node.toml with the testnet RPC and contract addresses
decdn setup --mbps 100 --region US --multiaddr /ip4/203.0.113.10/udp/4433/quic-v1
decdn-node run
decdn node health # then: node status, node lanes, node earnings, node topsetup generates the node key and keystore when neither exists, shows the operator terms,
bonds, registers the node, and prints a go/no-go summary
(ADR 019).
Bond size, pull-through, IPv6, and serving your own content
- Bond. The bond is
max(minBond, bondRequired(mbps)), wherebondRequiredis a curve over declared bandwidth, super-linear at the default exponent (ADR 026 Β§ Capacity-bond curve, Β§ Minimum bond). The testnet deployed withminBondat 50,000 TOKEN (manifest), which at the default curve (k = 12.6, Ξ± = 1.2) covers declarations up to about 997 Mbps; 1,000 Mbps needs about 50,162 TOKEN.decdn node bond --mbps <n> --dry-runprints the live target (it unlocks the keystore first). The maintainers send testnet TOKEN: send your EVM operator address (decdn whoamiprints it) through any channel on the contact page. - USDC. The node's own buyer pool pays for cache-miss pulls from peers (10 USDC working
deposit by default). The node spends it only with
cache.node_to_node_pull_through_enabledon, which is the default for a node with no origin backend and off for an origin node.decdn node statusshows the setting and the buyer wallet's balance. With it off, or with no USDC to open the pool, a cache miss the node cannot serve from its own backend returns not-found (runbook). Circle's testnet faucet dispenses it. - Keystore password. Set
DECDN_KEYSTORE_PASSWORDor pass--keystore-password-file(runbook). - Network. The node listens on UDP 4433 over IPv4 and IPv6, so a dual-stack host also
passes
--multiaddr /ip6/<addr>/udp/4433/quic-v1.--regionis an ISO 3166-1 alpha-2 country code (ADR 030). - Your own content. Import it into a local origin store with
decdn origin import --input <path> --to <dir>, then write the node config withdecdn config init --origin file://<absolute-dir>(--forcereplaces an existing one). For an S3 origin, import to a local directory andaws s3 syncit to the bucket: the two layouts are identical.
A publisher creates a namespace and seats the bonded operators that serve as its origins.
publish moves no bytes. A client passes the namespace with get decdn:<hash>?ns=<id>,
and a node that misses the blob pulls it from one of the seated operators' origin backends
(ADR 005 Β§ Namespace routing).
decdn publish namespace create # prints the namespace id
decdn publish assign <namespace-id> <0xOPERATOR>...The installed vetting policy decides who may seat origins
(ADR 011 Β§ Vetting policies). The testnet
runs the manual policy, so a VETTER_ROLE holder must vet the publisher's address before
assign succeeds. That step happens out-of-band, not through the CLI.
Local devnet
To run without testnet funds, contracts/dev-deploy.sh starts
Anvil and deploys the full protocol, a mock USDC, and a TOKEN faucet. See
CONTRIBUTING.md Β§ Local deployment.
The decdn-e2e tests are runnable end-to-end journeys: onboarding, paid fetch, and
publishing. They need cargo-nextest and Foundry, and they drive the debug binaries, so
build those first:
cargo build -p decdn-cli -p decdn-node
cargo nextest run -p decdn-e2e --features anvil-e2eββββββββββββββββββββ on-chain (Arbitrum) βββββββββββββββββββββ
β CapacityBond bonded TOKEN Β· registry of active nodes β
β PaymentPool USDC deposits Β· voucher redemption β
βββββββ²βββββββββββββββββββ²βββββββββββββββββββββββββββ²βββββββββ
β deposit Β· β bond Β· deposit Β· redeem β bond Β· redeem
β close Β· reclaim β β
βββββββ΄βββββββ ββββββββ΄ββββββββ βββββββββββββββ΄ββ ββββββββββββββββββββ
β Client βββββΊβ Cache node βββββΊβ Origin node ββββββ€ S3/R2/B2/HTTP/fs β
ββββββββββββββ ββββββββββββββββ βββββββββββββββββ ββββββββββββββββββββ
βββΊ bytes one way, USDC vouchers the other (cdn/client/v1)
- Nodes bond TOKEN against their declared bandwidth, register on-chain, then cache
and serve content. The node set is the
CapacityBondactive set, read directly from the contract (ADR 001). - Clients pick nodes from their peer store or the on-chain registry by measured RTT, stream content, and pay each node with signed vouchers drawn against one pool deposit (ADR 012, ADR 003).
- On a cache miss, a node fills from its own origin backend if it has one. With node-to-node pull-through on (the default for a node with no origin backend), it can also pull from a peer and stream to the client at the same time. The node pays for that upstream pull from its own pool, like any other client: every byte transfer is paid.
- Origin backends (S3, R2, B2, HTTP, filesystem) are private per-node config. No origin URL is ever exposed (ADR 002).
The node stack ships as two binaries, split like dockerd and docker
(appendix): decdn-node is the daemon, with one
subcommand, run; decdn is every command a human types. The sponsorship
daemon and its onramp are two more (docs/sponsord/). Run
decdn <command> --help for flags.
All decdn commands
decdn command |
Purpose |
|---|---|
key-gen, whoami |
Generate, then inspect, the Ed25519 node key and Ethereum keystore |
config {init,validate} |
Write a config file; check one before the daemon reads it |
setup |
Guided operator onboarding over key-gen, node bond, and node register |
node {β¦} |
Admin of a running daemon (health, status, lanes, pools, top, evict, reload, drain, slashes); the on-chain lifecycle (bond, unbond, register, deregister, rotate-key, update-multiaddrs, update-region); diagnostics (doctor, lookup) |
probe |
Probe a node over cdn/probe/v1 |
get |
Paid pull over cdn/client/v1 of what a decdn: URI names: a blob, a directory bundle, or one file of a bundle (appendix) |
quote |
Price a pull of a decdn: URI from the rates the nodes sign in their probe answers, before any paid byte |
receipt {verify,settle} |
Check the pull receipt get --receipt wrote, offline; add the on-chain redemptions that settle it (appendix) |
pool {β¦} |
Client payment-pool lifecycle: list, open, top-up, close, reclaim, assign |
publish {β¦} |
Publisher control plane: namespace create; seat and unseat authorized origins with assign and revoke |
origin {import} |
Write local content into a local filesystem origin store, offline and config-free |
appeal {slash} |
File a slash appeal and post the appeal bond (ADR 028) |
self-update |
Replace this binary with the latest release whose SHA256SUMS.minisig the embedded key verifies; --check only reports |
The global --config <path> selects the config file, and every decdn-node run flag also
reads a DECDN_* environment variable (decdn-node run --help). Every chain endpoint and
contract address comes from the config file, and each command also accepts it as a flag
(--rpc-url, --*-address). Operator terms accepted at registration live in
crates/cli/TERMS.md, embedded verbatim in the CLI.
For the full design, start at adr/README.md. It gives the reading order,
and adr/architecture.md is the living overview.
Wire protocols
| ALPN | Purpose |
|---|---|
cdn/probe/v1 |
Latency and availability probing |
cdn/client/v1 |
All paid delivery, clientβnode and nodeβnode |
cdn/dht/v1 |
Content discovery between nodes via a Kademlia DHT (ADR 022) |
Crates and contracts
crates/
node/ β daemon binary `decdn-node`: runtime bring-up, handlers, admin RPC server, dispatch limiter
cli/ β user CLI binary `decdn`
common/ β config schema + resolver, identity loading, AdminRpc trait + DTOs, the `decdn` clap tree and the daemon's `run` flags
protocol/ β wire format and ALPN message definitions (leaf crate)
config-types/ β config-vocabulary value types shared by cache + common (leaf crate)
bao-range/ β bao verified-range helpers and the origin-store layout (leaf crate, ADR 038)
cache/ β cache engine wrapping iroh-blobs + origin pull-through
client/ β `cdn/client/v1` paid-pull requester and buyer-side pool open, shared by node and cli
incentive/ β payment pools, staking, vouchers (alloy for Ethereum)
reputation/ β local per-peer reputation scoring (ADR 008)
e2e/ β test-only Rustβcontract fixtures (anvil + the production deploy script)
sponsord/ β sponsorship daemon binary `sponsord`: signs download capabilities against a PaymentPool, keeps it topped up
sponsord-onramp/ β public onramp binary `sponsord-onramp`: the gate, the `decdn.sh` / `decdn.ps1` installers, the hand-off
sponsord-core/ β the daemon's logic as a library, built on `client` and `incentive`
sponsord-api/ β sponsord wire types, error codes and typed HTTP clients (leaf crate)
sponsord-e2e/ β test-only: the daemon, the onramp and an installer run together
contracts/ β Solidity contracts + Foundry (excluded from the Cargo workspace)
The daemon is itself a paying client on its upstream cache-miss leg, so node depends on
client like any other consumer; so does sponsord-core, which opens the pool a sponsor
pays from. The leaf crates keep the publisher CLI, the client SDK and the sponsord servers
free of any blob store or AWS SDK. .github/scripts/check_crate_edges.py enforces that
boundary in CI.
The contracts cover operator bonding, USDC settlement and the fee split, buyback-and-burn,
served-bytes governance, slashing and appeals, content takedown, and publisher namespaces.
contracts/README.md lists each contract, and
ADR 016 Β§ Contract Inventory maps
how they interact.
Design at a glance
| Area | Design | ADRs |
|---|---|---|
| Content | Blobs are BLAKE3-addressed, and the receiver verifies every byte. The hash is a Merkle root, so any byte range verifies on its own, which makes resume and multi-source fetch safe. Publishers may encrypt content before upload; key distribution stays outside the protocol. | 002, 038 |
| Discovery | Clients never query the DHT: they pick bonded nodes from their peer store or the CapacityBond registry by measured RTT. Nodes find holders for a cache miss through the cdn/dht/v1 Kademlia DHT; for a namespaced request, the on-chain origin set is the directory of last resort. Latency-driven proxy warming creates regional copies where demand is. |
012, 022, 037 |
| Delivery | A large fetch spreads disjoint byte ranges across several holders, at most one per operator, and reassigns ranges as sources finish or stall. Cache admission and eviction are node-local. A node refuses a cache miss whose upstream price leaves no margin after the fee split. | 039, 040, 041 |
| Payments | A client funds one shared PaymentPool and pays each provider with cumulative off-chain vouchers. Only the provider redeems its own vouchers, so a pool owner's close cannot understate what a node earned. A close starts a grace window in which nodes keep redeeming, and each node's periodic sweep redeems well inside it. |
003 |
| Tokenomics | USDC pays for bytes. Operators bond TOKEN on a governance-tunable curve over declared bandwidth (super-linear at the default exponent), with a flat minBond floor. Fee-split buckets are governance-tunable. Protocol-owned liquidity is a Uniswap V3 50/50 TOKEN/USDC pool that governance seeds after launch. |
026, 016, 018 |
| Enforcement | Every ProbeResponse and StreamResponse carries a secp256k1 EIP-712 slash_sig, so a node's quotes and deliveries are on-chain evidence; the Ed25519 NodeId authenticates connections and the registration proof, never slash evidence. A challenge is a commitβreveal that resolves on-chain at reveal time. Slashed TOKEN sits in escrow for a 30-day appeal window. Governance blacklists content hashes. Reputation is a local per-peer EWMA. |
014, 028, 011, 008 |
| Governance | DecdnGovernor with a TimelockController. No TOKEN balance carries a vote. Vote weight is trailing-window served bytes, capped per epoch by declared capacity and overall by a per-operator share, ramped by tenure since first bond, and zeroed by a standing slash inside the trailing window. The process moves from admin key to bootstrap multisig to DAO; the contract set stays the same. |
009, 036 |
| Evolution | Varint-length framing, protocol enums, and a three-tier schema-evolution model. Production targets Arbitrum One. | 013, L2 appendix |
| Topic | Where |
|---|---|
| Project overview | decdn.org |
| User and operator docs | docs.decdn.org |
| Live testnet statistics | stats.decdn.org |
| Protocol specification (ADRs) and glossary | adr/, adr/glossary.md |
| Operating a node | Operator runbook, key rotation, upgrade path |
| Monitoring | Dashboards and alert rules (in decdn/devops); metric surface |
| Contracts | contracts/README.md |
| Sponsored downloads | docs/sponsord/: the daemon and onramp behind decdn.sh and decdn.ps1 |
| Releases | RELEASING.md: cutting, signing, and publishing a release |
CONTRIBUTING.md covers the environment, build, tests, code style, ADR conventions, the issue forms and the label scheme. Report security vulnerabilities privately as SECURITY.md describes.
Dual-licensed under either MIT or Apache-2.0, at your option.
