logicsrc/docs/openswarm/ipfile.md
Anthony Ettinger 4fed2681ec
Some checks are pending
CI / build (push) Waiting to run
test / test (push) Waiting to run
Add the LogicSRC OpenSwarm specification family (#142)
BitTorrent distributes bytes better than any CDN and has never been a
product: nobody is paid to seed, and nothing in it is private. Every
Profullstack media property answers that with a central HTTP proxy and a
pass system bolted on, and pays for every byte it serves.

OpenSwarm is an add-on to BitTorrent, carried as BEP 10 extension
messages, that fixes both. The swarm carries AES-256-CTR ciphertext whose
integrity is pinned by two SHA-256 merkle roots in a signed manifest, so
a tracker or DHT node learns an infohash and a size. A leecher buys a
pass over x402 in USDC (the same exchange x402-gateway runs for crawl
passes, settled by CoinPay), a seeder serves inside a bounded credit
window, and the leecher signs a cumulative voucher for every verified
batch. Whoever seeds gets paid. Vanilla clients remain valid members and
browsers remain first-class peers.

Adding a file mints a key pair for it, derived from one publisher seed by
default so there is one thing to back up: the public half is the file's
identity and its BEP 46 key, the private half signs the manifest and
authorises grants and payout changes, and a separate content key
encrypts the bytes and is sealed to paying peers.

The family: a core (records, keys, hashing, transports, discovery,
events), ipfile, ippay, ipdb (a signed hash-chained catalogue with heads
on the DHT), ipaudio, ipvideo, iplive (paid relays with backpressure)
and ipname (Moshpit pins and DNS TXT). Plus the c0mpute.com integration
with seven workload types and thirteen use cases, a proposed ip CLI,
conformance profiles, a security model and an FAQ.

Registered on the site the way OpenCreds is: nav entry, docs registry,
sitemap, a /openswarm landing page, PRD 0005, and the MCP prd_next_id
expectation moved to 0006.

Specs only. No code, no schemas, no reference implementation.


Claude-Session: https://claude.ai/code/session_01YafYxayh7Gqe5MWNNQMev2

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 15:05:57 -07:00

27 KiB

ipfile: Paid, Encrypted File Swarms

Version: 0.1 (draft) Status: draft. Extends the OpenSwarm core (spec.md). Nothing below is implemented.

1. Scope

ipfile is BitTorrent with three additions carried as extension messages:

  1. Encryption. The swarm carries ciphertext. The content key is sold separately and delivered only to a peer that has paid.
  2. Payment. A seeder serves pieces inside a bounded credit window and a leecher signs a cumulative voucher for every verified piece batch. Whoever seeds gets paid.
  3. Ownership. Every file has a key pair. The manifest that names the swarm, the price and the payout address is signed by it.

It is not a new transport. The wire is BEP 3, the extension is BEP 10, the hashes are BEP 52, discovery is BEP 5 and BEP 44, browsers are WebTorrent. A client that knows none of this can still join the swarm and, if the manifest allows, download ciphertext it cannot read.

Non-goals

Plaintext swarms (use a torrent). Hiding the size of a file. Hiding that a swarm exists (use swarm.private to keep it off the DHT, but a tracker still knows). Revoking a delivered key without re-encrypting. Streaming media semantics (those are ipaudio, ipvideo, iplive).

2. Terminology

Core terms apply. In addition:

Term Meaning
manifest The ipfile.manifest record: identity, roots, swarm, price, key delivery, policy.
revision A manifest with a higher rev under the same file key.
keeper A peer holding the content key with a delegate: true grant, allowed to grant it onward.
credit window Bytes a seeder will serve beyond the last voucher before choking.
voucher interval Bytes after which a leecher owes a new voucher.
vanilla peer A peer that did not advertise ipfile in its extension handshake.

3. Data model

3.1 The manifest

{
  "openswarm": "0.1",
  "type": "ipfile.manifest",
  "file": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
  "box": "x25519:4c4055604fe6bd8781cab6835093ad896f68f676b472d5bd6811e52f28cb18c3",
  "publisher": "ed25519:5d292428e8a68946e5225136c8b10e8f33ab78a45e1663d0730996dd2b63d59e",
  "fileId": "165aa20f2088f71a1880fe42c48d211b",
  "keyDerivation": "derived",
  "rev": 1,
  "name": "interview-2026-09-05.flac",
  "size": 734003200,
  "contentType": "audio/flac",
  "pieceLength": 1048576,
  "plainRoot": "sha256:d6c3f8285b7871d6a400cba14408288a9acde679f12e1e7dc276f29ca7c493ff",
  "cipherRoot": "sha256:ba8cd1a97b865a05cdc5840c63b086dd463624a831874c1ba0d7c4ce6f806f7a",
  "cipher": {
    "alg": "aes-256-ctr",
    "keyId": "sha256:41d10f45e705e0526c9eeedf62b32dda3daaf552dd9b6a7b09e01776b05813bb"
  },
  "swarm": {
    "infohashV1": "sha1:a3ce2180413415d7cf4268fb892b8ffd539e8459",
    "infohashV2": "sha256:4b74eb43677e4d03af5fb0856333f9aa21d9a5a3bbf944b13aa7eef379c7a342",
    "private": false,
    "trackers": [
      "wss://tracker.openwebtorrent.com",
      "wss://tracker.webtorrent.dev",
      "udp://tracker.opentrackr.org:1337/announce"
    ],
    "webseeds": [
      "https://gw.c0mpute.com/swarm/4b74eb43677e4d03af5fb0856333f9aa21d9a5a3bbf944b13aa7eef379c7a342/data"
    ]
  },
  "visibility": "public",
  "policy": {
    "vanilla": "ciphertext",
    "creditBytes": 4194304,
    "voucherBytes": 1048576
  },
  "pricing": {
    "currency": "USD",
    "perGib": "0.010000",
    "keyUsd": "0.500000",
    "split": { "publisherBps": 7000, "seederBps": 2500, "hubBps": 500 },
    "keeperBps": 1000,
    "hubs": ["https://coinpayportal.com/api/openswarm"],
    "payTo": { "network": "eip155:8453", "address": "0xCC3b072391AE7A8d10cF00DdC5F61DB2cA5541E5" }
  },
  "keys": {
    "modes": ["peer", "url"],
    "url": "https://keys.example.com/openswarm",
    "keepers": ["ed25519:d2d05fcad07ecbee6ff87c95159ec9969fce6c0fb9f41ff78c2c07ebe8a06c94"],
    "owner": "HjYr1barG9BmJK6DEBSm9Rl8iGeYTZFGwfyG8AJy5g3Wk8uP9nyG1IeDaHuOT/VgYoNMmGvlCnaWw4olY6xM/0GKdzr05DqWt49benOyvAs="
  },
  "replication": { "min": 3, "erasure": { "k": 10, "parity": 4 } },
  "meta": { "duration": 2941.2, "language": "en" },
  "createdAt": "2026-09-05T18:00:00.000Z",
  "updatedAt": "2026-09-05T18:00:00.000Z",
  "sigs": [
    {
      "alg": "ed25519",
      "key": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
      "sig": "RGRH7pDpXYcagULqj+8z05pLH99Lc6yLmfdbYgtGeRpjX5l0NcT3QSq9AWBtLw+5+JJiKoZ3s4ty74q/zBSE8w=="
    },
    {
      "alg": "ed25519",
      "key": "ed25519:5d292428e8a68946e5225136c8b10e8f33ab78a45e1663d0730996dd2b63d59e",
      "sig": "RGRH7pDpXYcagULqj+8z05pLH99Lc6yLmfdbYgtGeRpjX5l0NcT3QSq9AWBtLw+5+JJiKoZ3s4ty74q/zBSE8w=="
    }
  ]
}
Field Rule
file The file public key. REQUIRED. The record's first signature MUST be by this key.
box The file box public key (core §4.2). REQUIRED.
publisher The publisher identity. REQUIRED. A second signature by this key is REQUIRED.
fileId 16 bytes hex. REQUIRED. Stable across revisions.
keyDerivation derived (from the publisher seed, core §4.2) or standalone.
rev Integer, starts at 1, strictly increasing per file key.
predecessor, successor File keys, for rotation (core §4.5). OPTIONAL.
name Display name. Visible to anyone who obtains the manifest. MAY be empty.
size Plaintext byte length. Ciphertext has the same length.
contentType Media type of the plaintext.
pieceLength Power of two, at least 16384.
plainRoot, cipherRoot BEP 52 pieces roots (core §5.1).
cipher.alg aes-256-ctr in this version.
cipher.keyId sha256: of the content key. Identifies which key a grant carries without revealing it.
swarm.infohashV1, swarm.infohashV2 Of the hybrid torrent whose one file is the ciphertext (core §5.3).
swarm.private BEP 27 flag.
swarm.trackers, swarm.webseeds Announce URLs and BEP 19 URLs.
visibility public: any peer gets the manifest on request. pass: only a peer presenting a valid pass in scope.
policy.vanilla ciphertext: serve pieces to vanilla peers free. deny: choke them.
policy.creditBytes Credit window. Default 4194304.
policy.voucherBytes Voucher interval. Default 1048576. MUST be at most creditBytes / 2.
pricing See §7. keeperBps is taken from keyUsd, the split is applied to per-byte vouchers.
keys.modes Any of peer (a key holder in the swarm grants on request), url (an HTTPS key service).
keys.keepers Identities delegated to grant. Each MUST hold an ipfile.grant with delegate: true signed by the file key.
keys.owner The content key sealed to box. Lets the owner recover the key from seed plus manifest.
keys.public OPTIONAL. The content key itself, hex. Set only when the publisher wants anyone to decrypt (a free release, an ipdb segment). With it present, keyUsd MUST be 0.000000 and no grant is ever needed.
replication Publisher's request: at least min independent seeders. erasure is advice to storage nodes, never a wire format (§9).
meta Free-form. Member protocols define what they put here: ipaudio sets meta.track, ipvideo sets meta.title, ipdb sets meta.kind on segment files.

bps values are basis points; the three in split MUST sum to 10000.

3.2 The grant

A grant carries the content key to one recipient:

{
  "openswarm": "0.1",
  "type": "ipfile.grant",
  "file": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
  "keyId": "sha256:41d10f45e705e0526c9eeedf62b32dda3daaf552dd9b6a7b09e01776b05813bb",
  "grantor": "ed25519:d2d05fcad07ecbee6ff87c95159ec9969fce6c0fb9f41ff78c2c07ebe8a06c94",
  "grantee": "x25519:de3c5a1791131231f797726a59b46b5b877628f0d10ae97fb5f119faa1ad1dc5",
  "pass": "sha256:39655de63a29b02c90b236fa870fe6f8dfad1f52a8119fe4df85e88cd6ab02fd",
  "delegate": false,
  "expiresAt": null,
  "sealed": "KrGWWxD4blHwoh7UmJedgv/CdxWz2J9cbRZpMY+09mm6FT7exyU7KjEAXmWRTLh2Xr8INCGKhv6dpjScKFGCkCrPZDyfAHE2s4E5/VXc2zQ=",
  "createdAt": "2026-09-05T18:01:30.000Z",
  "sigs": [
    {
      "alg": "ed25519",
      "key": "ed25519:d2d05fcad07ecbee6ff87c95159ec9969fce6c0fb9f41ff78c2c07ebe8a06c94",
      "sig": "RGRH7pDpXYcagULqj+8z05pLH99Lc6yLmfdbYgtGeRpjX5l0NcT3QSq9AWBtLw+5+JJiKoZ3s4ty74q/zBSE8w=="
    }
  ]
}
Field Rule
grantor The file key, or a keeper listed in the manifest. A grant by anyone else is invalid.
grantee The recipient's box public key.
pass Record id of the pass that paid keyUsd. null only when delegate is true.
delegate True makes the grantee a keeper. Only the file key MAY sign a delegating grant.
expiresAt Keeper delegation SHOULD expire. A paid grant is perpetual: the peer holds the key.
sealed crypto_box_seal(contentKey, grantee), base64. 80 bytes for a 32-byte key.

A grantee decrypts sealed with its box private key and checks sha256(key) == keyId before using it.

4. Encryption

4.1 Piece encryption

The plaintext is split into pieces of pieceLength. Piece i is encrypted independently under AES-256-CTR:

iv(i)     = HKDF-SHA256(ikm = contentKey, salt = "", info = "openswarm:ipfile:iv:v1:" + decimal(i), L = 12) || 0x00000000
cipher(i) = AES-256-CTR(contentKey, iv(i), plain(i))

The 32-bit counter is big-endian and starts at zero, which is the WebCrypto AES-CTR shape with length: 32. A piece is at most 2^32 blocks of 16 bytes, so the counter never wraps below 64 GiB per piece.

CTR is chosen over an AEAD because it is length-preserving and seekable: a 16 KiB block at offset o in piece i decrypts from counter o / 16 without touching the rest of the piece. Integrity does not come from the cipher. It comes from two merkle trees:

  • The swarm verifies every block of ciphertext against cipherRoot through the torrent's own v2 piece layers. A tampered block is rejected by any BitTorrent v2 client before this specification is involved.
  • The decrypting client verifies every plaintext piece against the plaintext piece layer (§5.6), whose root is plainRoot in the signed manifest.

A malleable cipher whose output is pinned by a signed hash tree is not malleable in practice: any bit flip fails one of the two checks.

4.2 The content key

32 random bytes per file version. A new cipherRoot MUST use a new content key; reusing a key across two plaintexts under the same IV derivation would leak their XOR. The key never leaves a peer except inside a grant's sealed box, over a key service response, or in keys.owner.

5. Wire protocol

5.1 Extension handshake and hello

After the BitTorrent handshake, both peers send the BEP 10 extension handshake. A peer that supports this specification includes ipfile in m and "openswarm": "0.1". Immediately after, it sends hello:

{ "t": "hello",
  "v": 1,
  "id": <32 bytes, Ed25519 public key>,
  "box": <32 bytes, X25519 public key>,
  "nonce": <16 bytes>,
  "roles": ["seed"],
  "hubs": ["https://coinpayportal.com/api/openswarm"],
  "credit": 4194304,
  "interval": 1048576,
  "mtp": <optional 32 bytes, SHA-256 of the peer's MTP/1 SPKI>,
  "sig": <64 bytes> }

sig is Ed25519 over:

"openswarm:hello:v1" || infohashV2 (32 bytes) || own peer_id (20 bytes) || remote peer_id (20 bytes) || nonce

Binding both peer_ids and the infohash ties the identity to this connection in this swarm; a captured hello replays nowhere. roles is any of seed, leech, keeper, gateway. credit and interval are the window and the voucher interval this seeder will apply, which MUST be at least the manifest's policy values.

A peer MUST NOT send any other ipfile message before it has sent hello and received a valid one.

5.2 Messages

All payloads are bencoded dictionaries. r carries a record as UTF-8 JSON.

t Direction Payload Meaning
hello both §5.1 Identity and terms.
manifest_req any { "rev": int or absent } Ask for the manifest, optionally a specific revision.
manifest any { "r": bytes } The manifest record. Sent unsolicited by a seeder after hello when visibility is public.
layer_req leecher {} Ask for the plaintext piece layer.
layer seeder { "d": bytes } 32 bytes per piece, concatenated (§5.6).
pass leecher { "r": bytes } Present an ippay.pass.
credit seeder { "unpaid": int, "limit": int, "owed": string } Bytes served since the last accepted voucher, the window, and the amount owed.
voucher leecher { "r": bytes } An ippay.voucher.
key_req leecher { "keyId": bytes } Ask a key holder for a grant.
grant key holder { "r": bytes } An ipfile.grant sealed to the requester's box.
deny any { "what": string, "reason": string } Refuse pass, key_req or manifest_req with a registered reason.
bye any { "reason": string } About to disconnect.

Registered deny and bye reasons: no-pass, pass-invalid, pass-expired, pass-scope, pass-cap, unpaid, voucher-invalid, voucher-stale, not-keeper, no-key, visibility, rate, done.

5.3 The paid session

leecher                                   seeder
  |  BT handshake, BEP 10 handshake         |
  |---------------------------------------->|
  |<----------------------------------------|
  |  hello                                  |
  |---------------------------------------->|
  |<------------------------------- hello   |
  |<---------------------------- manifest   |   (public visibility)
  |  pass                                   |
  |---------------------------------------->|   seeder verifies hub signature,
  |<----------------------------- unchoke   |   scope, expiry, cap
  |  request / piece ... (vanilla)          |
  |<=======================================>|   up to `credit` bytes unpaid
  |<------------------------------ credit   |   unpaid >= interval: seeder asks
  |  voucher (cumulative)                   |
  |---------------------------------------->|   seeder verifies, unpaid resets
  |  request / piece ...                    |
  |<=======================================>|
  |  key_req                                |
  |---------------------------------------->|   if seeder is a key holder
  |<------------------------------- grant   |
  |  bye done                               |
  |---------------------------------------->|

Rules:

  1. A seeder MUST keep a leecher choked until a valid pass arrives, unless policy.vanilla is ciphertext, in which case an unpaid peer is treated as vanilla (§6) and gets no priority.
  2. A seeder MUST NOT serve more than credit bytes beyond the bytes covered by the last accepted voucher. When the window is full it chokes and sends credit. It MAY disconnect with bye unpaid after 60 seconds choked.
  3. A leecher MUST send a voucher when unpaid bytes reach interval, and only for bytes whose blocks it has verified against the v2 piece layer. A voucher is cumulative for this (pass, payee, swarm) triple (ippay §4).
  4. A seeder MUST verify the voucher's signature, its monotonic seq, that bytes is at least the previous voucher's bytes, and that the increase does not exceed what it actually served. A voucher that claims more than served is accepted for what was served and flagged; one that claims less is voucher-stale and rejected.
  5. A leecher MUST NOT sign a voucher for bytes it has not verified. It MAY sign for fewer than it has received; the seeder then chokes sooner. That is the only lever a leecher has against a slow seeder, and it costs nothing but throughput.
  6. Vouchers cover ciphertext bytes. Payment does not depend on the leecher ever obtaining the key.

5.4 Who may pay and who is paid

The voucher's payer is the pass's payer. The voucher's payee is the seeder's hello.id. A seeder MUST reject a voucher whose payee is not its own identity. The hub pays whoever redeems a voucher signed to them, so an identity is a payout account. A seeder that wants to be paid registers its identity and payout address at the hub once (ippay §6).

5.5 Key delivery

Peer mode. Any peer whose roles include keeper, or that is the file key itself, answers key_req. It MUST verify that the requester's pass is valid, in scope for this file, and has grant: true (meaning keyUsd was paid). It then signs and sends a grant sealed to the requester's box. A keeper MUST report the grant to the hub (ippay §5.4) to earn keeperBps and to let the hub mark the pass's grant as consumed; a pass buys one grant per file.

URL mode. GET <keys.url>/grant?file=<file key hex>&box=<x25519 hex> with Authorization: Bearer <pass token> returns the grant record as JSON. Without a pass it returns 402 with an x402 offer for keyUsd, so a client that has no hub relationship can still buy a key directly (ippay §3.4).

Owner recovery. The owner decrypts keys.owner with the file box private key derived from the seed.

5.6 The plaintext piece layer

The plaintext v2 piece layer is the sequence of per-piece SHA-256 merkle roots (32 bytes each) whose root is plainRoot. A seeder holds it because it had the plaintext or received it with the swarm. A leecher fetches it with layer_req (or from a gateway at /swarm/<infohashV2>/layer) and MUST verify that it hashes to plainRoot before trusting a single entry. After decrypting a piece the leecher checks it against the corresponding entry. For a 1 GiB file with 1 MiB pieces the layer is 32 KiB.

6. Coexistence with vanilla BitTorrent

A vanilla client sees a hybrid torrent with one file named by a hex string. It can download it if seeders allow. Specifically:

  • The extension handshake without ipfile marks the peer as vanilla.
  • With policy.vanilla: "ciphertext", a seeder treats it as any BitTorrent peer: normal choking algorithm, no credit accounting, no payment. It gets bytes it cannot decrypt. This is a free choice a publisher makes to widen the swarm; vanilla peers that then seed are unpaid redundancy.
  • With policy.vanilla: "deny", a seeder keeps it choked and MAY disconnect. It still answers ut_metadata so the peer can learn it has nothing to gain.
  • A vanilla peer never receives an ipfile message, because BEP 10 forbids sending an extension a peer did not advertise.
  • The DHT, trackers and PEX carry infohashes only. A public tracker learns a swarm exists and how many peers it has, exactly as today.
  • WebTorrent peers are vanilla until the page loads the ipfile extension. bittorrented.com's player would add it with wire.use().

Encryption of the wire itself (the RC4 "message stream encryption" some clients offer) is neither required nor relied on. Confidentiality of content comes from piece encryption; confidentiality of the link, where wanted, comes from the MTP/1 tunnel (core §6.5).

7. Pricing and settlement hook

The manifest's pricing is the whole price list:

Field Meaning
perGib USD per GiB of ciphertext served. Vouchers are computed from it (ippay §4.2).
keyUsd USD for one grant. 0.000000 makes the key free to any pass holder.
split Where each voucher's value goes: publisher, the seeder who served, the hub.
keeperBps Share of keyUsd paid to a keeper that grants. The rest of keyUsd goes to the publisher.
hubs Hubs the publisher accepts passes from. A seeder MUST reject a pass from a hub not listed.
payTo Where the publisher's share is paid. CAIP-2 network and an address.
royalties OPTIONAL. A list of { "label", "network", "address", "bps" } summing to 10000 that splits the publisher's share further. ipaudio and ipvideo records carry the same list; when both exist the manifest's wins, because the manifest is what the file key signed.

Passes are bought over x402 exactly as @profullstack/x402-gateway sells crawl passes today: a 402 whose body is CoinPay's x402 v2 offer, a proof in X-PAYMENT, verify and settle through CoinPay. ippay §3 has the exchange. A publisher who wants no payment at all sets perGib and keyUsd to 0.000000, and seeders serve for free with vouchers that carry zero; the accounting still runs so the publisher sees who served what.

8. Publishing procedure

ip file add <path> does, in order:

  1. Load the publisher seed. Generate fileId. Derive the file key and file box (core §4.2), or generate a standalone seed with --standalone-key.
  2. Generate the content key. Compute plainRoot and the plaintext piece layer. Encrypt piece by piece; compute cipherRoot.
  3. Build the hybrid torrent: one file named by cipherRoot hex, piece length as chosen, private per flag, announce-list from configuration, url-list for any gateway. Compute both infohashes.
  4. Seal the content key to the file box (keys.owner).
  5. Write the manifest with rev: 1. Sign with the file key, then the publisher key.
  6. Publish the BEP 44 pointer under the file key (core §7.2).
  7. Append an ipdb.entry for the manifest to the publisher's default feed (ipdb §4).
  8. Start seeding. Announce to trackers. If any keeper is configured, send it a delegating grant.

A revision repeats steps 2 to 7 with rev + 1 and the same fileId. A metadata-only revision (price, trackers, keepers) skips step 2 and keeps cipherRoot, keyId and the swarm.

9. Storage and serving on a c0mpute node

A c0mpute storage node stores whatever it likes internally: today that is blake3-addressed chunks with Reed-Solomon 10 data + 4 parity shards spread over hosts. On the wire it MUST present whole ciphertext pieces verified against cipherRoot. Erasure coding is a storage policy. replication. erasure in the manifest is the publisher saying what durability it is paying for in a pin job; it never changes what a peer sends.

A node that holds only shards reconstructs the piece before serving it. A node advertises c0mpute:role:storage and bids on ipfile.pin jobs (c0mpute.md §3). Pin income and voucher income are separate: the pin job pays for holding the bytes for a period, vouchers pay for serving them.

10. Retrieval procedure

ip file get <file key | infohashV2 | ip:// url>:

  1. Resolve to a manifest: BEP 44 get under the file key, or manifest_req from a peer, or a gateway.
  2. Verify both signatures. Verify sha256 of the canonical bytes matches any record id the caller supplied.
  3. Choose a hub from pricing.hubs the client has an account with. Buy or reuse a pass in scope (ippay §3). Include grant: true if the key is needed.
  4. Join the swarm. For each peer: exchange hello, send pass, download with sequential priority for streaming or rarest-first for bulk, sign vouchers on schedule.
  5. Fetch and verify the plaintext piece layer. Obtain a grant from a key holder or the key URL. Decrypt pieces as they complete; verify each against the layer.
  6. On completion, verify the whole against plainRoot. Send bye done.
  7. Keep seeding if configured. The client is now a seeder and earns.

11. Security notes specific to ipfile

  • Freeloading leecher. Bounded by creditBytes. A seeder loses at most one window per leecher identity per session and MAY refuse an identity it has seen freeload (ippay §5.5 reputation).
  • Freeloading seeder. Cannot happen: a voucher is only for bytes already received and verified. A seeder that stops after being paid was paid for what it sent.
  • Voucher inflation. A seeder cannot forge a voucher; it lacks the payer's key. A payer that over-states bytes pays more than it must; that is its own loss.
  • Double spend across seeders. A pass has a capUsd. A payer could sign vouchers to many seeders totalling more than the cap. The hub honours redemptions in order until the cap is reached and reports the rest as cap-exceeded; each seeder's exposure is still one window. A payer that does this is reported and its passes are refused (ippay §5.5).
  • Content key exposure. A paying peer holds the key. Any peer can leak it. This family prices access, it does not prevent copying. Rotation is the remedy for a leaked key going forward, and only forward.
  • Metadata. Size, piece length and both infohashes are public. name and meta are visible to anyone who obtains the manifest; visibility: "pass" restricts that to pass holders.
  • Manifest substitution. The BEP 44 pointer is signed by the file key and carries seq; a stale pointer is refused by the DHT itself. A manifest from a peer is verified against the file key the client started from.

12. Events

Type Emitted by data
com.logicsrc.openswarm.ipfile.manifest.published.v1 publisher record
com.logicsrc.openswarm.ipfile.manifest.revised.v1 publisher record
com.logicsrc.openswarm.ipfile.grant.issued.v1 key holder record (with sealed removed)
com.logicsrc.openswarm.ipfile.session.closed.v1 seeder { "swarm", "peer", "bytes", "vouchers", "reason" }

13. Implementations

Piece Exists Where What is new
Hybrid seeder announcing to wss trackers, browser WebTorrent player, range streaming Yes media-streamer (src/lib/streaming/streaming.ts, src/hooks/use-webtorrent.ts) The ipfile extension in wire.use(), piece decryption in the player
DHT observation of infohashes Yes media-streamer/services/dht-search-api (bitmagnet), dht-infohash-crawler Nothing; an ipfile swarm is an opaque infohash to a crawler
x402 pass sale and verification Yes x402-gateway, CoinPay /api/x402/verify and /settle Pass scope and vouchers (ippay)
Content-addressed chunk store, erasure coding, gateway by hash Yes c0mpute (c0mpute-store, c0mpute-gateway) Serving pieces by index verified by SHA-256, the pin workload
Sealed box key wrapping Yes logicsrc credentials team vaults, OpenCreds team profile The grant record
Manifest, file key derivation, piece encryption, hello, credit window, voucher exchange No All of it

14. Conformance

An ipfile implementation:

  1. Produces manifests per §3.1 with both signatures and a correct BEP 44 pointer.
  2. Encrypts and decrypts per §4 and verifies against both roots.
  3. Speaks every message in §5.2, refuses out-of-order messages per §5.1, and enforces §5.3 rules 2 to 5.
  4. Treats vanilla peers per §6 according to policy.vanilla.
  5. Issues grants only against a valid pass or a delegating grant per §5.5.
  6. Passes the published fixture swarm end to end: manifest, pass, three vouchers, grant, decrypted output matching plainRoot.

15. Version history

Version Date Change
0.1 2026-09-05 Initial draft.