Skip to main content

One channel per node you buy from

Each openChannel creates a channel between your address and one node’s address. Downloading from three different nodes in a session = three channels.
channels[i] = {
    id: H256,
    counterparty_node_address: 0x...,
    token: 0xUSDC,
    deposit: amount_usdc,
    opened_at_block: N,
    latest_voucher: Voucher { amount, nonce, sig },
}

Opening a channel

USDC.approve(PaymentChannel, deposit)
PaymentChannel.openChannel(counterparty, deposit, token)
Deposit sizing. The deposit bounds your maximum spend with this node in this session. Size it generously — unspent amount refunds on close, so overshoot is cheap. Under-sizing forces a mid-stream close + re-open, which wastes gas.

Streaming and vouchers

A voucher is a signed off-chain message:
Voucher {
    channel_id: H256,
    amount: U256,       // cumulative (not delta)
    nonce: u64,         // monotonic
    token: Address,
    signature: Eip712Sig,
}
Amount is cumulative — a voucher with amount=5 MB_USDC supersedes a voucher with amount=3 MB_USDC on the same channel. The node only needs to keep the highest-amount voucher; you can discard older ones too.

Voucher cadence

Default: sign a voucher every 1 MB. Configurable per stream:
decdn client fetch --hash X... --voucher-cadence-mb 10
Trade-off:
  • Small cadence (1 MB): more signatures, tighter bound on worst-case unpaid delivery if the node misbehaves. Baseline choice.
  • Large cadence (10 MB): fewer signatures, faster. Worst-case unpaid delivery is one cadence worth. For a 10 GiB download at 10 MB, that’s 1,024 signatures instead of 10,240 — meaningful CPU saving for hardware-signed workflows.
The node’s acceptable cadence is offered in StreamResponse.max_bytes_per_voucher. Pick the larger of your configured cadence and the node’s offer.

Closing a channel

Either party can initiate:
PaymentChannel.closeChannel(latest_voucher)
  • Happy path: node submits the latest voucher you signed. 48 h dispute window. No disputes → settles at voucher amount. Unspent deposit refunds to you.
  • If the node closes stale: you have the dispute window to submit a higher-nonce voucher via disputeChannel(voucher).
  • If you go offline: watchtowers can dispute for the node; for you, re-opening your client within the window lets your local dispute monitor handle it.
You are typically the one closing happy-path — when you’re done downloading, close yourself. Waiting for the node to close adds no value and leaves the deposit escrowed.

Dispute window

Default: 48 hours. Set by governance within the bounded range (12 h – 72 h). Why 48 h specifically: Arbitrum’s forced-inclusion path guarantees inclusion within ~24 h. 48 h gives you at least 24 h of effective response time even if the sequencer censors your dispute transaction for the worst-case period.

What could go wrong, and how it’s bounded

ThreatBound
Node serves corrupted bytesBLAKE3 verification rejects them before you pay for that chunk
Node streams and vanishes (no voucher accepted)At most one cadence worth of unpaid bytes delivered
Node closes with stale voucherYou submit the newer voucher within 48 h
You go offline during dispute windowLocal dispute monitor auto-handles while client is running; you lose only if you’re offline the entire window
Arbitrum sequencer censors you24 h forced-inclusion path; 48 h window absorbs the worst case

The local dispute monitor

Every client runs a background dispute monitor that watches on-chain events for its channels. Default config:
[dispute_monitor]
enabled = true
poll_interval_seconds = 60
Auto-submits disputeChannel(voucher) if it detects a stale close. No user interaction required if the client is running.

Running out of deposit mid-stream

If your cumulative voucher amount approaches the channel deposit:
  1. The node refuses new vouchers with StreamError::ChannelInsufficient.
  2. You close the current channel happy-path.
  3. You open a new channel with a larger deposit.
  4. You re-issue StreamRequest with offset_bytes = last_verified_byte.
This costs two extra on-chain transactions. Easier to over-size the deposit up front.

Token semantics

Multiple allowlisted ERC-20s are supported. Choose per-channel:
decdn client fetch --hash X... --payment-token USDT
Your client warns you if the token is not on the node’s advertised token_rates. Channels in delisted tokens can be force-closed by any party via forceCloseChannel — a safety net that ensures funds aren’t trapped.

Settlement invariants

  • Voucher signatures are the only settlement authority. Not your client, not the watchtower, not the counterparty.
  • Amounts are cumulative. Higher nonce + higher amount wins.
  • Tokens are fixed per channel. A voucher’s token field must match the channel’s token; cross-token disputes fail signature check.
  • Bounded settlement. A channel’s settlement can never exceed its deposit — over-signing vouchers is a client error, not a loss, because the on-chain function caps payout at deposit.
See trust model for what this means in the three-tier boundary.