{"id":"35d64b9dd439ebddd2a42ad80fde1602a8cbf51f00746f1f0ff87468a7339056","pubkey":"fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1","created_at":1787056543,"kind":30817,"tags":[["d","nkbip-07"],["title","Hierarchical Blossom Piece Indexes"],["summary","NKBIP-07: hierarchical kind 32176 indexes for multi-source Blossom files — ordered piece hashes via #b, ≤1000 a/b tags per event."],["k","32176"],["t","torrent"],["t","swarm"],["t","blossom"],["t","files"],["client","imwald"]],"content":"# NKBIP-07: Hierarchical Blossom Piece Indexes\n\nThis NKBIP defines a **parameterized replaceable index** kind `32176` for large binary payloads distributed via [Blossom](https://github.com/hzrd149/blossom) (content-addressed HTTP blobs), using a table-of-contents pattern analogous to [[NKBIP-01]] publication indexes. This creates a Nostr-native blob distribution similar to popular file distribution systems.\n\n## Motivation\n\nBlossom stores blobs by `sha256` on media servers. Swarm-like blob distribution needs:\n\n1. A **content id** and ordered **piece list**\n2. **Multi-source** fetch (many Blossom servers / mirrors)\n3. **Discovery** without putting file bytes on relays\n\nNKBIP-01 already solved hierarchical indexing for text via kind `30040` `a`-tags. NKBIP-07 reuses that shape for blob pieces.\n\nPiece hashes use a **single-letter** tag so relays that implement NIP-01 tag filters can answer queries such as `#b: [\"bc56e8d587133521b0da617b0475d18f9a8afacc45ae67549b8c2673273dec50\"]` (*which indexes contain this blob?*).\n\n## Event kind\n\n| Kind | Name | Role |\n|------|------|------|\n| **32176** | Blossom Piece Index | Parameterized replaceable hierarchical index |\n\nBlob bytes are **not** required in event `content`. They live on Blossom servers and are retrieved with `GET /<sha256>` (BUD-01).\n\n## Kind 32176: Blossom Piece Index\n\n### Requirements\n\n- `content` MUST be empty (`\"\"`) **or** contain a text transcript of the blob contents\n- If `content` is non-empty, a `transcript` tag SHOULD describe the format (`txt`, `vtt`, `srt`, or `json`). If that tag is missing or not one of those four, clients MUST treat the transcript as `txt`.\n- MUST include `d` (NIP-54-style: lowercase letters, numbers, hyphens)\n- MUST include `title`\n- Uniquely identified by `kind` + `pubkey` + `d`\n- Any `a` tags MUST be in **display / assembly order**\n- An index MAY have **zero** `a` tags (stub / announcement only)\n- Each index event MUST contain **at most 1000** `a` tags\n- Each index event MUST contain **at most 1000** `b` tags\n\n### `a` tag format\n\n```json\n[\"a\", \"<kind:pubkey:dtag>\", \"<relay hint>\", \"<label>\"]\n```\n\n- `[1]` — address of a child kind `32176` index\n- `[2]` — optional relay hint\n- `[3]` — optional legible label (**not** an event id)\n\n### Hierarchy\n\nEvery kind `32176` event is a **node** in a tree:\n\n| Role | Meaning |\n|------|---------|\n| **Root** | Entry index clients resolve by `d` / `naddr` |\n| **Branch** | Has one or more child indexes via `a` |\n| **Leaf** | Contributes piece bytes via `b` and has **no** child `a` tags needed for assembly |\n\n```text\nroot 32176\n  └─ branch 32176   (≤1000 a → branches or leaves)\n       └─ leaf 32176  (≤1000 b → piece hashes)\n```\n\nA node MAY be both **branch and piece-bearing**: it can carry local `b` tags **and** child `a` tags. There is no separate event kind for leaves.\n\nClients MUST walk with a cycle/depth guard (breadth-first or depth-first). Indexes MUST NOT form cycles.\n\n### Assembly order\n\nFor each node, clients MUST assemble bytes in this order:\n\n1. Local `b` tags, in tag order\n2. Then each child from `a`, in tag order (recurse)\n\nIf a node has only `b` tags, it is a leaf. If it has only `a` tags, it is a pure branch. If it has both, apply the order above.\n\n### Piece pointers (`b`)\n\nPieces MUST be listed with the single-letter tag `b` (NIP-01 `#b` discovery):\n\n```json\n[\"b\", \"<sha256>\", \"<length-bytes>\"]\n```\n\n- `[1]` — lowercase hex SHA-256 of the piece bytes\n- `[2]` — piece length in bytes (decimal string); SHOULD be present\n\n`b` tags MUST appear in assembly order. Within one tree, piece sizing conventions MUST be consistent. If a node would need more than **1000** pieces, split into additional leaf/branch children and reference them from the parent via `a`.\n\nEvery tree SHOULD also carry the full-file digest (distinct from per-piece `b`):\n\n```json\n[\"x\", \"<sha256-of-complete-file>\"]\n```\n\n`x` MAY be repeated on descendants. `size` (total bytes) SHOULD appear on the root when known.\n\n### Blossom servers\n\nIndexes MAY list preferred media servers:\n\n```json\n[\"blossom\", \"https://cdn.example\"]\n```\n\nClients SHOULD also consult the author’s kind `10063` Blossom server list (BUD-03) and any `r` mirrors. Fetch pieces with `GET /<sha256>` (BUD-01); optional `PUT /mirror` (BUD-04) after download to become another source.\n\n### Optional metadata tags\n\n| Tag | Meaning |\n|-----|---------|\n| `summary` | Short description |\n| `size` | Total file size in bytes (decimal string) |\n| `type` | Hint: `file`, `archive`, `media`, … |\n| `transcript` | Transcript format when `content` is not empty: `txt`, `vtt`, `srt`, `json`. Default `txt` if omitted |\n| `i` | External id (`sha256:…`, `magnet:…`, etc.) |\n| `t` | Topics |\n| `image` | Preview image URL |\n| `T` / `N` | Normalized title/author slugs (same convention as d-tags, see [[NKBIP-01]]) |\n\n---\n\n## Worked example: *Gone with the Wind* (1939)\n\nIllustrative Blossom tree for a ~3 GiB H.264 file of the film *Gone with the Wind*. Digests are **deterministic examples** (SHA-256 of label strings), not hashes of real copyrighted media.\n\n| Symbol | Value |\n|--------|--------|\n| Author pubkey | `3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690` |\n| Full-file `x` | `f40069245c728a3dbfab8ce4efee8f14d77b425c74a00f3ac06b810b166d6a83` |\n| MP4 init / header | `05b12ab19cb0f2567054543e061a65aea32cfc9514cc6589e61ab1776e4564ea` (4096 B) |\n| Piece 0 | `bc56e8d587133521b0da617b0475d18f9a8afacc45ae67549b8c2673273dec50` (1048576 B) |\n| Piece 1 | `41f0c7a44ba21e4532c46dc316fb822a3b71ecf4aa62daeb7919628cf5d8bfa2` (1048576 B) |\n| Piece 2 | `1b13f2bf2e8c265263cba6d73665e4a82b037fe7b4e194d3c187ac57644ea9d2` (1048576 B) |\n| Piece 3 | `59809cbdde09122d9398736a1b15a70655a4fc2ae7ba21025d4c2bace4d506f4` (1048576 B) |\n| Piece 4 | `a0b115beda4c834c8eb34b620cedf4387583f076802717d129bf407be2282eb4` (1048576 B) |\n| Piece 5 | `ba664b3efc97c992f973f02e4e196474bef26a9af1a19e1d6d9d69a7d4c1b890` (1048576 B) |\n| Total `size` | `3221225472` (3 GiB) |\n| Piece size | 1 MiB (`1048576`) → **3072** pieces |\n| Fan-out | ≤1000 `b` per leaf → **4** scene leaves (see tree) |\n\nTree (production shape) — children named for iconic scenes, in film order:\n\n```text\ngone-with-the-wind-1939                         (root)\n├─ …-twelve-oaks                                (leaf: 1000 × b)  Twelve Oaks barbecue\n├─ …-atlanta-burning                            (leaf: 1000 × b)  Burning of Atlanta\n├─ …-never-hungry-again                         (leaf: 1000 × b)  “I’ll never be hungry again”\n└─ …-frankly-my-dear                            (leaf: 72 × b)    Rhett’s farewell\n\ngone-with-the-wind-1939-mixed                   (optional: local header + scenes)\n├─ b: MP4 init segment (4 KiB)\n├─ a → twelve-oaks\n├─ a → atlanta-burning\n├─ a → never-hungry-again\n└─ a → frankly-my-dear\n```\n\nJSON samples below show only the **first few** `b` tags on each leaf; a real Twelve Oaks leaf would list **1000** `b` tags.\n\n### Example root\n\n```json\n{\n  \"kind\": 32176,\n  \"pubkey\": \"3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690\",\n  \"created_at\": 1735689600,\n  \"content\": \"\",\n  \"tags\": [\n    [\"d\", \"gone-with-the-wind-1939\"],\n    [\"title\", \"Gone with the Wind (1939)\"],\n    [\"T\", \"gone-with-the-wind-1939\"],\n    [\"N\", \"victor-fleming\"],\n    [\"x\", \"f40069245c728a3dbfab8ce4efee8f14d77b425c74a00f3ac06b810b166d6a83\"],\n    [\"size\", \"3221225472\"],\n    [\"type\", \"media\"],\n    [\"summary\", \"1939 epic; ~3 GiB H.264, 1 MiB Blossom pieces, scene-named leaves\"],\n    [\"t\", \"classic-film\"],\n    [\"t\", \"drama\"],\n    [\"image\", \"https://cdn.example/posters/gone-with-the-wind-1939.jpg\"],\n    [\"i\", \"sha256:f40069245c728a3dbfab8ce4efee8f14d77b425c74a00f3ac06b810b166d6a83\"],\n    [\"blossom\", \"https://cdn.example\"],\n    [\"blossom\", \"https://blossom.example\"],\n    [\n      \"a\",\n      \"32176:3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690:gone-with-the-wind-1939-twelve-oaks\",\n      \"wss://thecitadel.nostr1.com\",\n      \"Twelve Oaks\"\n    ],\n    [\n      \"a\",\n      \"32176:3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690:gone-with-the-wind-1939-atlanta-burning\",\n      \"wss://nos.lol\",\n      \"Atlanta Burning\"\n    ],\n    [\n      \"a\",\n      \"32176:3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690:gone-with-the-wind-1939-never-hungry-again\",\n      \"wss://relay.damus.io\",\n      \"I'll Never Be Hungry Again\"\n    ],\n    [\n      \"a\",\n      \"32176:3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690:gone-with-the-wind-1939-frankly-my-dear\",\n      \"wss://thecitadel.nostr1.com\",\n      \"Frankly, My Dear\"\n    ]\n  ]\n}\n```\n\nRoot address (replaceable coordinate):\n\n```text\n32176:3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690:gone-with-the-wind-1939\n```\n\n### Example leaf — Twelve Oaks (first ~1000 MiB)\n\n```json\n{\n  \"kind\": 32176,\n  \"pubkey\": \"3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690\",\n  \"created_at\": 1735689601,\n  \"content\": \"\",\n  \"tags\": [\n    [\"d\", \"gone-with-the-wind-1939-twelve-oaks\"],\n    [\"title\", \"Gone with the Wind — Twelve Oaks\"],\n    [\"x\", \"f40069245c728a3dbfab8ce4efee8f14d77b425c74a00f3ac06b810b166d6a83\"],\n    [\"b\", \"bc56e8d587133521b0da617b0475d18f9a8afacc45ae67549b8c2673273dec50\", \"1048576\"],\n    [\"b\", \"41f0c7a44ba21e4532c46dc316fb822a3b71ecf4aa62daeb7919628cf5d8bfa2\", \"1048576\"],\n    [\"b\", \"1b13f2bf2e8c265263cba6d73665e4a82b037fe7b4e194d3c187ac57644ea9d2\", \"1048576\"],\n    [\"blossom\", \"https://cdn.example\"]\n  ]\n}\n```\n\n*(Real event: 1000 `b` tags for this scene’s byte range; only the first three are shown.)*\n\n### Example leaf — Atlanta Burning\n\n```json\n{\n  \"kind\": 32176,\n  \"pubkey\": \"3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690\",\n  \"created_at\": 1735689602,\n  \"content\": \"\",\n  \"tags\": [\n    [\"d\", \"gone-with-the-wind-1939-atlanta-burning\"],\n    [\"title\", \"Gone with the Wind — Atlanta Burning\"],\n    [\"x\", \"f40069245c728a3dbfab8ce4efee8f14d77b425c74a00f3ac06b810b166d6a83\"],\n    [\"b\", \"59809cbdde09122d9398736a1b15a70655a4fc2ae7ba21025d4c2bace4d506f4\", \"1048576\"],\n    [\"b\", \"a0b115beda4c834c8eb34b620cedf4387583f076802717d129bf407be2282eb4\", \"1048576\"],\n    [\"b\", \"ba664b3efc97c992f973f02e4e196474bef26a9af1a19e1d6d9d69a7d4c1b890\", \"1048576\"],\n    [\"blossom\", \"https://cdn.example\"]\n  ]\n}\n```\n\n*(Real event: next 1000 pieces; digests above stand in for the first three of that range.)*\n\nAssembly from the **root** (no local `b`): Twelve Oaks ‖ Atlanta Burning ‖ I’ll Never Be Hungry Again ‖ Frankly, My Dear (3072 × 1 MiB = 3 GiB).\n\n### Example mixed branch (init segment + scenes)\n\nUseful when the first bytes are an MP4/fMP4 init segment kept on the parent:\n\n```json\n{\n  \"kind\": 32176,\n  \"pubkey\": \"3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690\",\n  \"created_at\": 1735689610,\n  \"content\": \"\",\n  \"tags\": [\n    [\"d\", \"gone-with-the-wind-1939-mixed\"],\n    [\"title\", \"Gone with the Wind (1939) — mixed index\"],\n    [\"x\", \"f40069245c728a3dbfab8ce4efee8f14d77b425c74a00f3ac06b810b166d6a83\"],\n    [\"size\", \"3221225472\"],\n    [\"type\", \"media\"],\n    [\"b\", \"05b12ab19cb0f2567054543e061a65aea32cfc9514cc6589e61ab1776e4564ea\", \"4096\"],\n    [\n      \"a\",\n      \"32176:3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690:gone-with-the-wind-1939-twelve-oaks\",\n      \"wss://thecitadel.nostr1.com\",\n      \"Twelve Oaks\"\n    ],\n    [\n      \"a\",\n      \"32176:3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690:gone-with-the-wind-1939-atlanta-burning\",\n      \"wss://nos.lol\",\n      \"Atlanta Burning\"\n    ],\n    [\n      \"a\",\n      \"32176:3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690:gone-with-the-wind-1939-never-hungry-again\",\n      \"wss://relay.damus.io\",\n      \"I'll Never Be Hungry Again\"\n    ],\n    [\n      \"a\",\n      \"32176:3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690:gone-with-the-wind-1939-frankly-my-dear\",\n      \"wss://thecitadel.nostr1.com\",\n      \"Frankly, My Dear\"\n    ]\n  ]\n}\n```\n\nAssembly: **init ‖ Twelve Oaks ‖ Atlanta Burning ‖ I’ll Never Be Hungry Again ‖ Frankly, My Dear**.\n\n### Example with captions (`transcript`)\n\n`content` holds searchable WebVTT; media bytes stay on Blossom:\n\n```json\n{\n  \"kind\": 32176,\n  \"pubkey\": \"3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690\",\n  \"created_at\": 1735689620,\n  \"content\": \"WEBVTT\\n\\n00:00:01.000 --> 00:00:04.000\\nFrankly, my dear, I don't give a damn.\\n\",\n  \"tags\": [\n    [\"d\", \"gone-with-the-wind-1939-captions-en\"],\n    [\"title\", \"Gone with the Wind (1939) — English captions\"],\n    [\"transcript\", \"vtt\"],\n    [\"type\", \"media\"],\n    [\"T\", \"gone-with-the-wind-1939-captions-en\"],\n    [\"x\", \"f40069245c728a3dbfab8ce4efee8f14d77b425c74a00f3ac06b810b166d6a83\"],\n    [\"b\", \"bc56e8d587133521b0da617b0475d18f9a8afacc45ae67549b8c2673273dec50\", \"1048576\"],\n    [\"blossom\", \"https://cdn.example\"]\n  ]\n}\n```\n\n## Client algorithm (normative sketch)\n\n1. Resolve root `32176` by `d` / `naddr`.\n2. Recursively process each node: emit local `b` pieces in order, then recurse each `a` child in order (≤1000 children per node); reject cycles.\n3. Download missing pieces in parallel from Blossom servers; verify each SHA-256.\n4. Concatenate; verify full-file `x` if present.\n5. Optionally mirror pieces to a configured Blossom server.\n\nExample: [Imwald web client](https://jumble.imwald.eu/notes/naddr1qvzqqqrakqpzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqyghwumn8ghj7mn0wd68ytnvv9hxgtcpzfmhxue69uhkummnw3eryvfwvdhk6tcqz3nkc7txv9jxztt9wejku6twvukhg6tyv5xyzjz5)\n\n## Relationship to other specs\n\n* **NKBIP-01** — same empty-content hierarchical `a`-index pattern for publications (`30040` / `30041`).\n* **Blossom BUD-01 / BUD-03 / BUD-04** — blob GET, server lists, mirroring.\n* **NIP-01** — single-letter `#b` filters for piece-hash discovery.\n* **Not BitTorrent** — no info-hash/DHT requirement; swarming is multiserver (and optional user Blossom nodes) over HTTP.\n\n## Review notes (non-normative)\n\n* Prefer **empty `content`** for the video bytes; put searchable dialogue in a separate captions index with `transcript` (as above), or keep VTT only in a leaf event. Missing `transcript` means `txt`.\n* `#b` is the discovery hook; `#x` is useful when relays index it, but not all do — do not rely on `#x` alone.\n* Keep piece size conventions consistent inside one tree (example: 1 MiB media pieces).\n* Cap of **1000** `a` / **1000** `b` per event forces fan-out for multi-GB films (here four scene leaves for a 3 GiB encode), same scaling idea as NKBIP-01 section indexes.\n* Prefer human `a` labels / `d` suffixes people recognize (scene names) over opaque `part-000` counters when the split is editorial, not only mechanical.\n","sig":"7e58e382ba11db97926e65b2f1421846aaf4d6ca428fcdf004129d7d401fa1ebe621652b4d0c46d447b16c9c5d956a921a05c9098bce7187652aa383f98d0922"}