mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-09-10 19:26:00 +00:00
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>
This commit is contained in:
parent
93af4770ac
commit
4fed2681ec
21 changed files with 4609 additions and 1 deletions
545
docs/openswarm/ipfile.md
Normal file
545
docs/openswarm/ipfile.md
Normal file
|
|
@ -0,0 +1,545 @@
|
|||
# ipfile: Paid, Encrypted File Swarms
|
||||
|
||||
Version: **0.1** (draft)
|
||||
Status: draft. Extends the OpenSwarm core ([spec.md](./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
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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_id`s 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. |
|
||||
Loading…
Add table
Add a link
Reference in a new issue