What a HiveRelay operator can and cannot do, in terms of math rather than trust. If you're an operator weighing what you're signing up for, or an app developer choosing relays, this is the document to point at.
A relay's trust surface depends on the content mode.
- For public/non-blind apps, a relay may store readable public Hypercore blocks and catalog metadata. Do not use public mode for data that must remain confidential from the operator.
- For blind-mode apps and atomic custody payloads, the relay stores ciphertext and does not receive the decryption key. The operator can still see network metadata such as peer pubkeys and timing.
Across those modes, the relay cannot:
- Forge an app or its updates (publisher signature required, verified by every reader)
- Pass proof-of-relay/storage-proof challenges without the challenged bytes and matching cryptographic proof material
- Decrypt a blind-mode app or custody payload even if it stores the ciphertext forever (the encryption key is never sent to relays)
This isn't a promise from the relay operator. It's enforced by the protocol; a malicious operator who tried to violate any of the above would either fail the cryptographic check or simply not produce the result. Apps and clients verify, they don't trust.
Every Hyperswarm connection — peer-to-peer or peer-to-relay — is wrapped in
a Noise XK handshake (hypercore-protocol) before any application data
flows. The handshake provides:
| Property | Means |
|---|---|
| Confidentiality | A passive observer (including the relay forwarding the bytes) sees ciphertext only — random-looking bytes |
| Integrity | An active attacker can't tamper with bytes in flight without breaking the MAC; receiver detects and tears down the connection |
| Forward secrecy | If the long-term keys leak later, past sessions remain unreadable (ephemeral keys are discarded after handshake) |
| Mutual authentication | Both sides prove possession of their static keys; the connection is identity-pinned |
This applies to the Hypercore replication protocol and to every Protomux sub-channel layered on top (seed-request, circuit-relay, proof-of-relay, service-RPC, app-catalog). Passive observers and relays that only forward opaque circuit traffic see Noise frames. A relay that is the replication endpoint decrypts its own transport session and may store the resulting public blocks unless the app content is encrypted before replication.
Wire encryption protects data in transit from passive observers and from relays that are merely forwarding opaque circuit traffic. A relay that intentionally seeds a public/non-blind app also stores the app's public blocks, so wire encryption is not a confidentiality guarantee against that relay's local disk. Use blind mode or application-level encryption when the relay operator must not read content.
Source: hypercore-protocol README, Noise Protocol Framework spec.
Hypercores are Merkle-tree-backed append-only logs. Each block is signed by the publisher's keypair. Readers verify the signature against the public key (the appKey) before accepting the block. A relay that stored forged blocks would simply fail signature verification on the reader side — forged content cannot enter the chain.
| Layer | Crypto |
|---|---|
| Block authenticity | Ed25519 signature over the Merkle root by the publisher key |
| Position integrity | Every block references the previous Merkle root — blocks cannot be reordered or omitted without detection |
| Write authority | Only the holder of the publisher private key can append |
Implication for relays: a relay cannot inject content into an app's feed. For non-blind/public apps, it may be able to read the public blocks it stores; for blind-mode apps, it stores ciphertext. In both cases, the worst integrity failure it can cause is refusal or selective service, which readers route around by querying other peers.
For apps that publish in blind mode (client.publish(content, { encryptionKey })),
content is encrypted with a 32-byte symmetric key before it enters the
Hypercore. The relay stores the encrypted blocks as opaque bytes and has
no way to decrypt them.
| What relays see | What they don't |
|---|---|
| Encrypted block ciphertext | Plaintext content |
| The appKey (Hypercore public key) | The encryption key (never transmitted to relays) |
| Block sizes and rough write timing | Block contents |
| Peer connection metadata (which pubkeys connected) | Reader identity beyond pubkey |
The encryption key is shared peer-to-peer (out of band, or through the app itself once a reader authenticates). The relay never holds it.
Concrete guarantee: even an operator with full filesystem access to their own relay storage holds nothing more than encrypted blocks they can't decrypt. The HTTP gateway returns 403 for blind apps to enforce this at the application layer too — there's no "view in browser" path that would require decryption.
Seed requests carry a signature from the app's publisher key over
(appKey || 'seed' || timestamp || maxStorageBytes). Relays verify the
signature before honoring the request. This means:
- Anyone can ask a relay to seed an app, but only the app's publisher can authenticate the request as "I'm the one publishing this"
- Replay protection: timestamps must be within a 5-minute window of the relay's clock, and signatures are cached for dedup
- An unsigned seed request can still be accepted (depending on acceptMode), but it carries no publisher attestation — the operator is trusting the network's reputation system to surface bad actors
Unseed requests (the kill switch) require the same publisher signature. A relay won't drop content on a third party's say-so.
A relay can be challenged to produce a Merkle proof for a specific block of content it claims to be serving. The challenger picks a random block, the relay returns the block + its inclusion proof, the challenger verifies against the publisher's signed Merkle root.
What this proves:
- The relay actually has the bytes (otherwise it can't produce the proof)
- The bytes are the correct ones (otherwise the inclusion proof fails)
What this does not prove:
- That the relay will serve the bytes to a different peer at request time (it can selectively answer)
- That the relay holds the bytes in memory — it could fetch on demand from another peer and proxy
That's why proof-of-relay is paired with bandwidth receipts (signed proofs
of bytes actually transferred to a counterparty) and, where enabled, sampled
storage-proof service challenges for a richer "this relay is serving" signal.
See core/protocol/proof-of-relay.js,
core/protocol/bandwidth-receipt.js, and
services/builtin/storage-proof-service.js.
How a client confirms that a specific relay genuinely holds and serves an app — without trusting the relay's self-reported catalog. There are two tiers, layered, each anchoring its trust in the drive key's signed Merkle root rather than in the relay's word.
const v = await client.verifySeeded(driveKey, { relay: relayPubkeyHex })
// { complete, relayIsPeer, relayHasFullLength, contentVerified,
// metaLength, blobsLength, relayRemoteLength }The client opens the drive, confirms the relay is a live peer advertising the full length, and downloads both of the drive's Hypercores (metadata + blobs) to completion. Hypercore verifies every block against the drive key's signed Merkle root on arrival — a forged or substituted block fails verification, so the relay cannot fake content it isn't the author of.
| Field | Means |
|---|---|
contentVerified |
The genuine, complete content downloaded and verified against the drive key |
relayIsPeer |
The relay is a live peer of the drive |
relayHasFullLength |
The relay advertises the full metadata length |
complete |
relayHasFullLength && contentVerified |
What this does not prove. Replication rides the shared swarm, so
contentVerified proves the content is genuine and served by the swarm,
and relayHasFullLength is the relay's own advertised state. It is not a
per-block, relay-attributable, third-party-portable proof. For that, Tier 2.
const r = await client.proveSeeded(driveKey, { relay: relayPubkeyHex, samples: 5 })
// { ok, driveKey, relay, head, passed, total,
// samples: [{ index, valid, reason }, …] }
// r.ok === true only if EVERY sampled block verified at the current headThe client opens the drive (learning the metadata head), samples up to 16
random block indices, and for each one challenges the relay with a fresh
32-byte nonce over the existing service RPC (storage-proof.prove). Each
signed proof is verified against an isolated, key-only verifier core. Two
independent checks, neither trusting the relay:
| Check | Crypto |
|---|---|
| Content | A real Hypercore block proof; the key-only verifier confirms the block hashes into the drive key's signed Merkle root — forged content is rejected |
| Attribution + freshness | The relay signs coreKey || u32le(index) || nonce || blake2b(block) with its swarm identity key — the proof is attributable to this relay and bound to this nonce, so a recorded proof can't be replayed |
| Length pin | The proof's author-signed upgrade.length is checked against the current head (minLength), rejecting a relay stuck on an old, shorter signed version |
Relay-side, buildStorageProof reads local storage only — it throws
BLOCK_NOT_LOCAL / BLOCK_OUT_OF_RANGE rather than fetch-on-demand to answer
a challenge.
Honest limitation. This is a challenge-response proof-of-retrievability, not a sealed proof-of-replication. A relay could in principle fetch a block on demand rather than store it. Random-index sampling across the full core plus a latency bound make that expensive and detectable — but it is not cryptographically precluded. Sealed PoRep is out of scope. (v1 proves the drive's metadata core; blobs-core proofs are a follow-up.)
Privacy gate (blind drives). The relay-side service is opt-in and off
by default (Node: config.plugins / Services tab; Bare/appliance:
config.services or HIVERELAY_STORAGE_PROOF=1). Critically, a blind or
privacy-redacted drive returns NOT_SEEDED — indistinguishable from a key
the relay genuinely doesn't hold. A signed proof is cryptographic,
relay-attributable evidence of possession; serving it for a blind drive would
turn prove() into a possession oracle that defeats the catalog's deliberate
redaction (see "Blind-mode apps" above). The service also carries a
sybil-resistant global proof-work rate cap and a phantom-core DoS guard
(it only proves keys already in the relay's app registry — never store.get
on a caller-supplied key).
| Attack | What happens |
|---|---|
| Read app contents (non-blind) | Possible — the operator stores plaintext blocks. Use blind mode if you don't want this. |
| Read app contents (blind mode) | Impossible — they only have ciphertext, no key |
| Inject forged content into an app | Impossible — Ed25519 signature verification on every block |
| Modify blocks they're storing | Detectable — Merkle root mismatch |
| Refuse to serve content | Possible — but other relays / peers will serve it (this is why replication-factor matters) |
| Lie about serving in proof-of-relay | Impossible — they have to produce real Merkle proofs against real bytes |
| Fake holding a seeded app under proof-of-retrievability | Cannot forge content — signed block proofs must hash into the drive key's signed root; can't fake attribution either (relay signature over a fresh nonce). Can only fetch-on-demand instead of storing (PoR limit) |
| Surveil who connects | Possible — they see counterparty pubkeys (this is the network-metadata threat). Mitigations: Tor transport, ephemeral keys per session |
| Censor specific apps from their catalog | Possible — that's the whole point of accept-modes. The operator chooses what they carry. Other relays may still carry it. |
| Attack | Impact |
|---|---|
| Submit floods of seed requests | Mitigated by accept-modes (Review queues, Closed rejects) and per-relay rate limits |
| Try to push forged content under someone else's appKey | Fails — signature check |
| Try to use a relay as an open DDoS amplifier via DHT-relay-WS | Mitigated by maxConnections per transport plus API and endpoint rate limits; transport-specific abuse controls still remain an operational tuning surface |
| Try to read another app's blind content | Fails — they don't have the encryption key |
| Property | HiveRelay (blind mode) | Cloud (S3 / Lambda / Vercel) |
|---|---|---|
| Operator can read your data | No | Yes (or service role can) |
| Operator can subpoena your data | Only ciphertext available | Yes, plaintext |
| Operator can be coerced to forge content | No (signature) | Yes (vendor controls signing keys) |
| Operator visibility into who reads | Pubkeys only, can be ephemeral | IP addresses, often tied to identity |
| Single point of failure | No (multiple replicas) | Vendor outage |
| Compliance/data-residency claims | Operator chooses what to store | Vendor SLA |
This isn't an argument that HiveRelay is unconditionally better — there are plenty of apps where vendor-managed makes more sense. It's an argument that the trust surface is genuinely smaller for apps that fit the model.
- Anonymity. A relay sees the pubkey of every peer that connects. That's a
network-metadata leak. If you need anonymity, layer Tor (
transports/tor/) or use ephemeral keypairs per session. - Censorship resistance against state actors. A nation-state can fingerprint and block Hyperswarm DHT traffic. We provide the tools (Tor, blind mode, federation) but don't claim impunity against well-funded adversaries.
- Long-term unbreakable encryption. Curve25519 / Ed25519 / ChaCha20-Poly1305 are state-of-the-art today. They could conceivably be broken in 30 years by quantum computers. Nothing in HiveRelay specifically addresses post-quantum, and that's an honest limitation.
packages/core/core/protocol/seed-request.js— signature schemepackages/core/core/protocol/proof-of-relay.js— challenge/response mathpackages/core/core/protocol/bandwidth-receipt.js— signed transfer proofspackages/core/core/protocol/proof-of-storage.js— Tier-2 signed proof-of-retrievability (legacystorage-proofroute, build/verify)packages/services/builtin/storage-proof-service.js— Tier-2 relay service (storage-proof.prove, privacy gate, rate caps)packages/client/index.js—client.verifySeeded(Tier 1) andclient.proveSeeded(Tier 2)packages/services/identity/attestation.js— Schnorr attestations for dev-key → app-key bindings- hypercore docs — upstream cryptography
- Noise Protocol XK pattern — wire encryption