logicsrc/docs/openswarm/iplive.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

17 KiB

iplive: Live Streams over Paid Peers

Version: 0.1 (draft) Status: draft. Extends the OpenSwarm core (spec.md). Reuses ipfile's encryption, grants and credit window and ippay's passes and vouchers. Adds one BEP 10 extension, iplive. Nothing below is implemented.

1. Scope

iplive moves a live stream from one origin to many viewers through peers that are paid to relay it:

  1. A channel record: identity, renditions, latency mode, price, relay policy.
  2. A head record: signed every segment, naming the segments that exist and the key epoch in force.
  3. The iplive extension: announcing, requesting and fanning out encrypted segments with backpressure.
  4. Roles: origin, relay, viewer, and how a relay is paid.
  5. Recording: how a finished stream becomes an ipvideo title.

Non-goals

Ingest protocols (RTMP and SRT are the encoder's business; c0mpute's DIP 0019 covers them). Sub-second latency; the floor is one part duration. Chat. Ads.

2. Terminology

Term Meaning
channel A named live source with a key pair derived from the publisher seed.
channel id 20 bytes: SHA-256 of `"openswarm:iplive:v1"
origin The peer that encodes, encrypts, signs heads and holds the content key.
relay A peer that forwards segments it has verified and is paid per byte.
viewer A peer that plays. It MAY also relay.
segment One CMAF fragment, 2000 ms by default, encrypted as one unit.
part In low-latency mode, a sub-segment chunk (500 ms default) announced before the segment completes.
key epoch The span of segments encrypted under one content key.
live edge The newest segment the origin has announced.

3. Data model

3.1 Channel

The channel key derives like a feed:

channelSeed = HKDF-SHA256(ikm = publisher seed, salt = utf8("live:" + name), info = "openswarm:live:v1")
{
  "openswarm": "0.1",
  "type": "iplive.channel",
  "channel": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
  "box": "x25519:4c4055604fe6bd8781cab6835093ad896f68f676b472d5bd6811e52f28cb18c3",
  "publisher": "ed25519:5d292428e8a68946e5225136c8b10e8f33ab78a45e1663d0730996dd2b63d59e",
  "name": "courtside",
  "title": "Courtside, live",
  "latency": "low",
  "segmentMs": 2000,
  "partMs": 500,
  "epochSegments": 900,
  "dvrSegments": 1800,
  "renditions": [
    { "id": "720p", "codecs": "avc1.64001f,mp4a.40.2", "width": 1280, "height": 720, "frameRate": 30, "bitrateKbps": 2800 },
    { "id": "360p", "codecs": "avc1.64001e,mp4a.40.2", "width": 640, "height": 360, "frameRate": 30, "bitrateKbps": 800 }
  ],
  "swarm": {
    "trackers": ["wss://tracker.openwebtorrent.com", "udp://tracker.opentrackr.org:1337/announce"],
    "private": false
  },
  "relays": { "allow": "any", "maxDownstream": 8, "minRelays": 2 },
  "policy": { "vanilla": "deny", "creditBytes": 8388608, "voucherBytes": 2097152 },
  "pricing": {
    "currency": "USD",
    "perGib": "0.020000",
    "keyUsd": "1.000000",
    "split": { "publisherBps": 6000, "seederBps": 3500, "hubBps": 500 },
    "keeperBps": 0,
    "hubs": ["https://coinpayportal.com/api/openswarm"],
    "payTo": { "network": "eip155:8453", "address": "0xCC3b072391AE7A8d10cF00DdC5F61DB2cA5541E5" }
  },
  "keys": { "modes": ["peer"], "keepers": [] },
  "createdAt": "2026-09-05T20: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
channel, box Channel signing and box keys. Signed by both channel and publisher, as an ipfile manifest is.
latency normal: segments announced when complete. low: parts announced as they are encoded.
segmentMs, partMs 2000 and 500 by default. 4000 is allowed and matches c0mpute DIP 0019.
epochSegments Segments per content key. 900 at 2 s is 30 minutes.
dvrSegments Segments the origin and relays keep for late joiners and scrubbing.
relays.allow any, or a list of identities allowed to pull from the origin.
relays.maxDownstream Peers one relay will feed at once.
relays.minRelays Relays the origin wants before it stops feeding viewers directly.
policy, pricing, keys As ipfile §3.1 and §7, with the same fields and meanings. seederBps pays relays.

The record lives in ipdb under channel:<channel key>.

3.2 Head

Signed by the channel key every time a segment (or, in low-latency mode, a part) is announced:

{
  "openswarm": "0.1",
  "type": "iplive.head",
  "channel": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
  "seq": 4187,
  "prev": "sha256:ba8cd1a97b865a05cdc5840c63b086dd463624a831874c1ba0d7c4ce6f806f7a",
  "epoch": 4,
  "keyId": "sha256:41d10f45e705e0526c9eeedf62b32dda3daaf552dd9b6a7b09e01776b05813bb",
  "live": true,
  "segments": [
    {
      "seq": 4187,
      "t": 8374000,
      "d": 2000,
      "renditions": [
        { "id": "720p", "root": "sha256:d6c3f8285b7871d6a400cba14408288a9acde679f12e1e7dc276f29ca7c493ff", "size": 702113, "parts": [175528, 175530, 175527, 175528] },
        { "id": "360p", "root": "sha256:4b74eb43677e4d03af5fb0856333f9aa21d9a5a3bbf944b13aa7eef379c7a342", "size": 200418, "parts": [50104, 50105, 50104, 50105] }
      ]
    }
  ],
  "init": [
    { "id": "720p", "root": "sha256:c9f17d4eaf2b28122b46d111cef6697d2c3f708a19d4628bce5c0304481b355b", "size": 1524 },
    { "id": "360p", "root": "sha256:00bc9c01ca00938016e276fe5b5748cfe13fd02bc53cc651535682c8dd5c83be", "size": 1498 }
  ],
  "relays": ["ed25519:d2d05fcad07ecbee6ff87c95159ec9969fce6c0fb9f41ff78c2c07ebe8a06c94"],
  "createdAt": "2026-09-05T22:19:34.000Z",
  "sigs": [
    {
      "alg": "ed25519",
      "key": "ed25519:0d87e09c7fea3ad6ba6c2f3e027ea47f5b245452899910948470906704c5295d",
      "sig": "RGRH7pDpXYcagULqj+8z05pLH99Lc6yLmfdbYgtGeRpjX5l0NcT3QSq9AWBtLw+5+JJiKoZ3s4ty74q/zBSE8w=="
    }
  ]
}
Field Rule
seq Segment number of the live edge. Starts at 1 when the stream starts.
prev Id of the previous head, so a viewer can detect a skipped announcement.
epoch, keyId Which content key encrypts segments from this seq on. A key change is announced one full segment before it applies.
segments The newest segment, or the newest few. A head is small: relays fill history from have.
segments[].renditions[].root BEP 52 pieces root of the segment ciphertext, with the segment as one piece. That is what a relay verifies before forwarding.
segments[].renditions[].parts Byte lengths of each part, in order, for low-latency requests.
init Roots and sizes of each rendition's init segment, which is not encrypted.
relays Identities the origin currently feeds; a viewer prefers these.
live false in the final head, which marks the end of the stream.

Heads are not stored in ipdb. The final head is, under channel:<key>:end:<seq>, alongside the recording (§8).

3.3 Segment encryption

As ipfile §4.1, with the segment as a single piece and the IV derived with info = "openswarm:iplive:iv:v1:" + decimal(seq). The content key changes each epoch; a grant carries keyId and a viewer requests a new grant when the head announces a new keyId. A pass expiring mid-stream stops at the next epoch, which is why epochs are short.

4. Wire protocol

4.1 Finding peers

Peers announce the channel id to the DHT (announce_peer) and to the channel's trackers exactly as they would an infohash. Vanilla clients that see it find a swarm whose ut_metadata returns nothing; they leave. Browsers find WebRTC peers through wss trackers as with any WebTorrent swarm.

4.2 Handshake

The BitTorrent handshake uses the channel id as the infohash. The BEP 10 handshake advertises iplive. Both sides send hello with the ipfile §5.1 fields (identity, box, nonce, roles, hubs, credit, interval, sig) where roles is any of origin, relay, viewer, and the signature covers the channel id in place of infohashV2.

4.3 Messages

t Payload Meaning
hello as ipfile Identity and terms.
channel { "r": bytes } The channel record. Sent by origin and relays after hello.
head { "r": bytes } A head record. Forwarded to every downstream peer within 100 ms of receipt.
have { "from": int, "to": int, "r": [string] } Segment range and rendition ids the sender holds.
window { "buffer": int, "live": int, "down": int, "max": int } Segments buffered, live edge, downstream count, maxDownstream. Sent after hello and whenever it changes.
want { "seq": int, "r": string, "part": int or absent } Request one segment, or one part.
seg { "seq": int, "r": string, "part": int or absent, "o": int, "d": bytes } A 16 KiB chunk of the requested unit at offset o.
init { "r": string, "d": bytes } An init segment.
pass, credit, voucher, key_req, grant, deny, bye as ipfile Payment and keys, unchanged.

seg chunks are 16 KiB so that a WebRTC data channel and an MTP/1 record both carry one without fragmentation.

4.4 Session

viewer                                    relay
  |  hello                                  |
  |---------------------------------------->|
  |<------------------------------- hello   |
  |<----------------------------- channel   |
  |<------------------------------ window   |   buffer=30 live=4187 down=3 max=8
  |  pass                                   |
  |---------------------------------------->|
  |<-------------------------------- head   |   seq 4187
  |<-------------------------------- have   |   4158..4187
  |  want 720p init                         |
  |---------------------------------------->|
  |<-------------------------------- init   |
  |  want 4185 720p                         |
  |---------------------------------------->|
  |<--------------------------------- seg   |   x 43 chunks
  |  want 4186 720p ...                     |
  |  key_req                                |
  |---------------------------------------->|   relay is not a keeper:
  |<-------------------------------- deny   |   not-keeper; ask origin or keys.url
  |  voucher                                |
  |---------------------------------------->|

Rules:

  1. A viewer starts segmentMs * 2 behind the live edge in normal mode and one part behind in low mode.
  2. A peer MUST NOT want a seq below live - buffer of the peer it asks. If it has fallen further behind it jumps to live - 1 and discards. Stalls are resolved by dropping, never by holding a relay's buffer.
  3. A relay forwards a segment only after verifying its ciphertext against the root in a head signed by the channel key. A relay never needs the content key.
  4. A relay serving down >= max answers new hellos with deny rate and the current head, whose relays list points elsewhere.
  5. Payment is ipfile §5.3 rules 2 to 6, with a relay as payee. The default window is larger (8 MiB) because a segment is bigger than a piece and a choke at the live edge is a stall.
  6. Keys come from the origin or a keeper listed in the channel, never from a plain relay. A relay MAY be a keeper if the publisher delegates to it.

4.5 Low latency

In low mode the origin announces a head as soon as the first part of a segment is encoded, with parts filled in as they complete (lengths for finished parts, 0 for pending). A viewer requests parts by index and appends them to MSE as they arrive. A part is encrypted as part of the segment's CTR stream: part k begins at counter offset(k) / 16, so a relay can forward parts without waiting for the segment and a viewer can decrypt them in order. The segment root is only known when the segment completes; a relay in low mode forwards parts on the strength of the signed head naming the part lengths and verifies the root when the last part lands, disconnecting any upstream whose completed segment fails.

5. Roles and payment

Role Holds key Pays Is paid
Origin Yes Nothing Publisher share of every voucher, keyUsd per grant
Relay No Vouchers to its upstream for bytes it pulled Vouchers from downstream for bytes it served
Viewer Yes, after a grant Vouchers to whoever served it Vouchers if it also relays

A relay's margin is seederBps of what it serves minus what it pays upstream. With perGib = 0.020000, a relay feeding eight viewers from one upstream pull earns 8 * 0.35 * 0.02 = 0.056 and pays 0.02 per GiB pulled, before the hub's share. That is the incentive to relay, and a c0mpute node bids on iplive.relay jobs for the guaranteed floor (c0mpute.md §3).

The origin is a peer that pays nobody; relays.allow and minRelays are how it limits its own upload. A publisher who wants more reach than its relays provide buys pins on c0mpute.

6. Publishing procedure

ip live create courtside --latency low writes the channel record and puts it in ipdb. ip live start courtside --input srt://:9000 then:

  1. Derives the channel key. Generates epoch 1's content key.
  2. Runs the encoder (ffmpeg) producing CMAF segments per rendition, and for low mode, parts.
  3. For each completed unit: encrypts, computes the root, signs a head, announces to the DHT and trackers, sends head to every connected peer.
  4. Answers key_req with grants for pass holders.
  5. Rotates the content key every epochSegments, announcing the new keyId one segment early.
  6. On stop: signs a final head with live: false, and if --record, assembles the recording (§8).

7. Viewer procedure

ip live watch ed25519:<channel key> or a browser page:

  1. Resolve the channel record from ipdb or a peer.
  2. Buy or reuse a pass scoped to the channel key with grant: true.
  3. Find peers; prefer identities in the latest head's relays; connect to two.
  4. Get the init segment, the head, a grant. Start at the live edge minus the offset in §4.4 rule 1.
  5. Pull each segment from the connected peer with the smaller live - seq lag; on a deny rate or a stall over one segment duration, replace the peer.
  6. Send vouchers on schedule to each peer that served bytes.

8. Recording

With --record, the origin keeps every segment's plaintext, concatenates each rendition into one CMAF file, and publishes an ipvideo.title whose segment index is exactly the head history. Because segments were 2 or 4 seconds and keyframe-aligned, the recording is a valid ipvideo ladder with no re-encode. The title's ipdb entry references the channel in meta.channel and the final head's id.

9. Security and privacy

  • Relays see ciphertext and heads. They learn segment sizes and timing, never content.
  • Head forgery needs the channel key. A relay forwarding an unsigned or wrongly signed head is dropped by every downstream peer.
  • Freeloading is bounded per ipfile §11 with the larger window.
  • Denial by relay count: an origin with relays.allow: any can be surrounded by relays that pull and never serve. It pays them nothing (it pays nobody) and they earn nothing; the cost is its upload. maxDownstream on the origin is the cap.
  • Late key: a viewer whose pass expires keeps the current epoch's key and loses the next. Epoch length is the publisher's tolerance.

10. Events

Type Emitted by
com.logicsrc.openswarm.iplive.channel.created.v1 publisher
com.logicsrc.openswarm.iplive.stream.started.v1 origin
com.logicsrc.openswarm.iplive.stream.ended.v1 origin, with the final head id and recording title id
com.logicsrc.openswarm.iplive.relay.joined.v1 origin

11. Implementations

Piece Exists Where What is new
RTMP/SRT ingest to HLS segments, segment swarming in the DHT, relay nodes rebuilding playlists, LL-HLS opt-in Designed c0mpute dips/0019-live-stream-plugin.md Encrypted segments, signed heads, paid relays: the same design with ippay attached
Live TV playback in the browser (hls.js, mpegts.js) Yes media-streamer hls-player-modal.tsx, tipoffwatch and genrewatch players MSE append of decrypted CMAF parts from peers instead of an HLS URL
Pay-per-game passes with live session caps Yes (central) media-streamer IPTV resale (iptv_share_sessions) Concurrency is no longer the owner's provider line; it is the origin's maxDownstream
Channel and head records, iplive messages, relay economics, recording to ipvideo No All of it

12. Conformance

An origin signs a head per unit, rotates keys per epoch, and announces a key change one segment early. A relay verifies roots before forwarding, honours window rules, and never serves below live - buffer. A viewer drops rather than stalls, vouchers every peer that served it, and requests a grant on a keyId change.

13. Version history

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