--- bsip: 11 title: Delegated Communities description: How a community keeps its owner key offline while an online minter key publishes for it, and how readers verify what the minter signs. author: Tommaso Casaburi (@tomcasaburi) discussions-to: https://github.com/bitsocialnet/BSIPs/issues status: Draft type: Standards Track category: Core created: 2026-10-23 requires: 2, 3, 4, 5, 10 --- ## Abstract A **delegated community** separates the key that identifies it from the key that publishes it. The owner holds the **anchor** key offline; its key address is the community's identity. An online node holds a **minter** key and signs the community record, its comment updates, and its challenge messages. The owner signs one long-lived IPNS record that points the anchor's IPNS name at the minter's; the minter's IPNS record points at the current community record, which carries a signed `anchor` claim naming its identity. This BSIP specifies that chain, the `anchor` field, the exact algorithm a reader uses to load and verify a community record (ordinary or delegated) and the comment updates signed under delegation, how a delegated community's name must be configured, and how the owner sets up and rotates minters. ## Motivation A community's record must be re-signed every time its content changes, so the signing key has to live on an always-online machine. If that key is also the community's identity, then whoever runs the machine, a hosting provider or an RPC operator, can take the community over permanently, and a leaked key loses the address for good. Delegation lets the owner keep the identity key offline, hand day-to-day publishing to a replaceable delegate, and revoke that delegate by signing one new record, without changing the community's address or invalidating any content it has published. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). ### Keys A delegated community uses two Ed25519 keypairs: - **Anchor** (`As` private, `An` its key address, [BSIP-10](./bsip-10.md#key-addresses)). `An` is the community's identity key: its address when it has no name, the `communityPublicKey` of every publication to it, and the target of its name record. `As` is held by the owner, SHOULD stay offline, and MUST NOT be given to the node that publishes the community. - **Minter** (`Ms` private, `Mn` its key address). `Ms` is held by the online node that runs the community. It signs everything the community publishes. An **ordinary** (non-delegated) community has a single keypair that plays both roles. Every rule in this BSIP reduces to the ordinary rules of [BSIP-3](./bsip-3.md#signing) when `An` equals `Mn`. ### The delegation chain ```mermaid flowchart TB As(["Owner, offline: anchor key As"]) Ms(["Node, online: minter key Ms"]) AR["IPNS record at /ipns/An
Value: /ipns/Mn
validity: about 100 years"] MR["IPNS record at /ipns/Mn
Value: /ipfs/<cid>"] CR["Community record <cid>
anchor.publicKey = An
signature by Ms"] CU["Comment updates,
CHALLENGE, CHALLENGEVERIFICATION"] As -. signs .-> AR Ms -. signs .-> MR Ms -. signs .-> CR Ms -. signs .-> CU AR --> MR --> CR ``` A delegated community is published as a chain of exactly two IPNS records and one content record: 1. The **anchor record**: an IPNS record ([IPNS Record specification](https://specs.ipfs.tech/ipns/ipns-record/)) for the name `An`, signed by `As`, whose Value is the ASCII string `/ipns/` followed by `Mn` in base58btc. Its validity (EOL) SHOULD be far in the future (RECOMMENDED: about 100 years), because the record stops resolving everywhere once it expires and the owner is offline by design. 2. The **minter record**: an IPNS record for the name `Mn`, signed by `Ms`, whose Value is `/ipfs/` of the current community record. It is republished as the community changes. Its TTL SHOULD be one publish interval of the node. 3. The **community record** ([BSIP-3](./bsip-3.md)): signed by `Ms`, carrying the `anchor` claim. An ordinary community has no anchor record: its IPNS record for its key address points directly at `/ipfs/`, and its community record is signed by that key and carries no `anchor`. A chain MUST NOT contain more than one `/ipns/` → `/ipns/` hop. Readers MUST reject a longer chain. Decoded view of the two IPNS records (the record signatures are omitted): ```jsonc // IPNS record for /ipns/12D3KooWNsF2eMrdw3YFPycAaFEvZ9Eq3ekhNL3vrAAfN6dAwXLp (An), signed by As { "Value": "/ipns/12D3KooWFtYXbGeyPTajVxUZRuf4sT73j2DS2EGcY5CAmgggLCbV", "ValidityType": 0, "Validity": "2126-09-29T00:00:00.000000000Z", "Sequence": 0 } // IPNS record for /ipns/12D3KooWFtYXbGeyPTajVxUZRuf4sT73j2DS2EGcY5CAmgggLCbV (Mn), signed by Ms { "Value": "/ipfs/QmYNCCoJoYdYpHDp5fbC1RZfBZd2orKXDdj7UWjUwEsbLD", "ValidityType": 0, "Validity": "2026-10-25T00:00:00.000000000Z", "Sequence": 1742, "TTL": 20000000000 } ``` ### The `anchor` field A community record MAY carry: - `anchor` — object. Present exactly when the community is delegated. It MUST contain exactly one property: - `publicKey` — string, the key address `An` in base58btc ([BSIP-10](./bsip-10.md#key-addresses)). This is not the base64 raw key that `signature.publicKey` and `encryption.publicKey` carry. `anchor` is an ordinary signed field: like every field of the record it MUST be listed in `signature.signedPropertyNames`. A node that publishes an ordinary community MUST omit it. A reader MUST treat a record whose `anchor` has any other shape as invalid. Example delegated community record (abbreviated): ```jsonc { "name": "news.bso", "anchor": { "publicKey": "12D3KooWNsF2eMrdw3YFPycAaFEvZ9Eq3ekhNL3vrAAfN6dAwXLp" }, "title": "News From Trusted Sources", "challenges": [{ "type": "text/plain", "description": "Answer a question." }], "encryption": { "type": "ed25519-aes-gcm", "publicKey": "WjgogrxE2Bb7ybz41YawZJpWpaQVeIqrvGQFvs0TcuQ" }, "pubsubTopic": "12D3KooWFtYXbGeyPTajVxUZRuf4sT73j2DS2EGcY5CAmgggLCbV", "createdAt": 1792713600, "updatedAt": 1792800000, "statsCid": "Qm…", "protocolVersion": "1.0.0", "posts": { "pages": { "hot": { "comments": [ // each comment carries "communityPublicKey": "12D3KooWNsF2…AwXLp" (An), never Mn ] } } }, "signature": { "type": "ed25519", "signature": "", "publicKey": "WjgogrxE2Bb7ybz41YawZJpWpaQVeIqrvGQFvs0TcuQ", "signedPropertyNames": ["posts", "challenges", "encryption", "name", "anchor", "createdAt", "updatedAt", "pubsubTopic", "statsCid", "protocolVersion", "title"] } } // signature.publicKey and encryption.publicKey derive to Mn = 12D3KooWFtYXbGeyPTajVxUZRuf4sT73j2DS2EGcY5CAmgggLCbV ``` ### Which key signs what | Object | Signed by | A reader requires | | --- | --- | --- | | Anchor record (IPNS) | `As` | a valid IPNS record for the name `An` | | Minter record (IPNS) | `Ms` | a valid IPNS record for the name `Mn` | | Community record | `Ms` | signer address equal to the chain's terminal name (`Mn`) | | `encryption.publicKey` | (a field of the record) | the raw key of `Ms`: its key address equals the record's signer address | | Comment updates, in every form | `Ms` | `signature.publicKey` equal to that of the current community record | | `CHALLENGE`, `CHALLENGEVERIFICATION` ([BSIP-4](./bsip-4.md)) | `Ms` | signer address equal to the key address of `encryption.publicKey` | | Comments, votes, edits, moderation | their authors | `communityPublicKey` equal to `An` | Nothing that readers verify is signed by `As` except the anchor record. ### Loading and verifying a community record This algorithm applies to every community, ordinary or delegated. Its input is a key address `N` to fetch: the community's identity key when the reader already holds one, otherwise the key the reader was given, otherwise the key its name resolved to ([BSIP-10](./bsip-10.md#verifying-a-community-name)). #### Resolving the chain 1. Let `hops = [N]`. 2. Obtain the IPNS record for the last element of `hops` and read its own Value; do not let a resolver collapse several hops into one answer. The record MUST be valid for that name under the IPNS Record specification: its signature MUST verify against the key the name commits to, and its validity MUST NOT have passed. A reader MUST reject an expired record, including an expired anchor record. 3. If the Value is `/ipfs/`, the chain is complete. 4. If the Value is `/ipns/`: if `hops` already has two elements, reject the chain. Otherwise append `M` and repeat from step 2. 5. Reject any other Value. The result is `hops`, either `[N]` or `[N, M]`, and the `cid` of the community record. `hops[0]` is the first name walked and the last element is the chain's **terminal**. A reader that fetches through an HTTP gateway it does not trust MUST NOT accept a chain the gateway resolved on its behalf. It MAY fetch `GET /ipns/` first: if the served community record's signer address equals `N`, the chain is `[N]` and the record's own signature binds it to `N`. Otherwise it MUST fetch every IPNS record of the chain itself with `GET /ipns/?format=ipns-record` (`Accept: application/vnd.ipfs.ipns-record`), validate each as in step 2 and reject any larger than 10 KiB, confirm that the terminal record's CID equals the CID of the served body, and confirm that the body's signer address equals the terminal. #### Proving an unwalked claim Fetch the record by `cid` and parse it ([BSIP-3](./bsip-3.md)). If `hops` is `[N]`, the record carries `anchor`, and `anchor.publicKey` (call it `A`) is not `N`, nothing walked so far confirms the claim. The reader MUST then resolve the chain of `A` as above, over the same transport, and require that its terminal equals `N`. On success it replaces `hops` with that chain, `[A, N]`. On failure the record is **not endorsed**: the reader MUST reject it, MUST NOT adopt any value from it (identity, topic, encryption key, or freshness), and SHOULD NOT retry the same record. The proof establishes the delegation `A → N`, not a particular CID, so an honest claim does not fail because the minter published a newer record between the two resolutions. #### Verifying the record Given the record `R` and `hops`, the reader MUST reject `R` unless all of the following hold: 1. `R` matches what the reader asked for, as specified in [BSIP-10](./bsip-10.md#verifying-a-community-name). 2. Every field of `R` other than `signature` is listed in `signature.signedPropertyNames`, and `R` carries no runtime-only field ([BSIP-10](./bsip-10.md#wire-fields)). 3. If `hops` has two elements, `R` carries `anchor` and `anchor.publicKey` equals `hops[0]` exactly. 4. The signature verifies under [BSIP-2](./bsip-2.md#signatures). 5. The signer address of `R` equals the terminal of `hops`. 6. The key address derived from `encryption.publicKey` equals the signer address of `R`. 7. Every comment and comment update embedded in `R` (for example `posts.pages`) verifies under [Pages](#pages) and [Comment updates under delegation](#comment-updates-under-delegation), using the identity key `I` defined below. The identity key is `I = R.anchor.publicKey` when `R` carries `anchor`, otherwise `I = hops[0]`. #### Adopting the record - The reader MUST ignore `R` unless its `updatedAt` is greater than the `updatedAt` of the record it currently holds for the community. An older or equal record is not an error. - The reader sets the community's identity key to `I`. If `I` differs from the identity key it held before (it reached the record through the minter and the claim moved the identity to the anchor), it MUST discard any name outcome computed for the previous identity and verify the name again against `I` ([BSIP-10](./bsip-10.md#verifying-a-community-name)). - Later fetches MUST start from `I`, not from the minter and not from a key that a name pointed at. The anchor record is the authoritative, rotation-safe pointer. - Publications use `I` as `communityPublicKey`, and the challenge exchange uses `R.encryption` and `R.pubsubTopic` ([Challenge exchange](#challenge-exchange)). #### Verification summary **Ordinary record**, fetched from `N`: 1. The IPNS record for `N` is valid, unexpired, and its Value is `/ipfs/`. 2. `R` has no `anchor`, or `anchor.publicKey` equals `N`. 3. If loaded by name, `R.name` is the same name ([BSIP-10](./bsip-10.md#verifying-a-community-name)). 4. All fields are signed, none is runtime-only, and the signature verifies. 5. The signer address of `R` equals `N`, and so does the key address of `encryption.publicKey`. 6. Embedded pages verify against identity `N`, and embedded comment updates are signed by `R.signature.publicKey`. 7. `R.updatedAt` is newer than the record held. **Delegated record**, fetched from `N`: 1. Walk at most one hop from `N`, validating every IPNS record, to `/ipfs/`. 2. If only `N` was walked and `R.anchor.publicKey` is `A ≠ N`, the chain of `A` must end at `N`; use `[A, N]` as the chain. Otherwise reject without adopting anything. 3. With a two-element chain, `R.anchor.publicKey` equals its first element. 4. If loaded by name, `R.name` is the same name. 5. All fields are signed, none is runtime-only, and the signature verifies. 6. The signer address of `R` equals the chain's terminal (`Mn`), and so does the key address of `encryption.publicKey`. 7. The identity is `An = R.anchor.publicKey`. Embedded pages verify against it, including the anchor rule, and embedded comment updates are signed by `R.signature.publicKey`. 8. `R.updatedAt` is newer than the record held. Later fetches start from `An`. ### Names of delegated communities A delegated community's name record ([BSIP-10](./bsip-10.md#resolving-bso-and-eth-names)) MUST point at the anchor `An`, never at a minter. Before adopting a name, the node that runs the community SHOULD resolve it and refuse the name unless it resolves to `An`. A reader whose name lookup returns the community's own minter treats the name as **refuted** and keeps the identity; this is a misconfiguration, not a key migration ([BSIP-10](./bsip-10.md#verifying-a-community-name)). Such a reader can still load the community: it fetches through `Mn`, proves the record's claim ([Proving an unwalked claim](#proving-an-unwalked-claim)), and adopts `An`. A name that resolves to any other key triggers a [key migration](./bsip-10.md#key-migration). ### Publications, pages, and comment updates #### Publications Every publication to a delegated community MUST carry `communityPublicKey` equal to `An`. The node MUST reject a publication labelled with any other key. It SHOULD give a distinct rejection reason when the label is its own minter `Mn`, which indicates a client that addressed the serving key instead of the identity. The node labels everything it stores with `An`, so stored content stays valid after a minter rotation. #### Pages When the community record carries `anchor`, rule 1 of [Community membership of comments](./bsip-10.md#community-membership-of-comments) applies to every comment in its pages and reply pages, whether or not the record has a `name`: a comment's `communityPublicKey`, when present, MUST equal `anchor.publicKey`. A comment labelled with a minter key is not a member. #### Comment updates under delegation Comment updates ([BSIP-5](./bsip-5.md)) are signed by the minter `Ms`. This covers standalone updates fetched through `postUpdates`, updates embedded in pages and in moderation-queue pages, and the reduced updates returned in `CHALLENGEVERIFICATION`. To verify a comment update, a reader MUST: 1. require that every field other than `signature` is listed in `signedPropertyNames`, that no reserved field is present, and that the signature verifies ([BSIP-2](./bsip-2.md#signatures)). A reader MAY skip the signature check for an update embedded in a community record or page whose integrity it has already established, but MUST still apply step 2; 2. require that the update's `signature.publicKey` equals the `signature.publicKey` of the community record it currently holds and has verified, which is `Ms`, not `An`; 3. apply the `cid` binding and the `edit` signer check of [BSIP-5](./bsip-5.md); and 4. verify the update's `replies` pages against the identity `An` under [Pages](#pages), applying these same steps to every nested update. A reduced update inside `CHALLENGEVERIFICATION` arrives in a message signed by the minter ([Challenge exchange](#challenge-exchange)); a reader SHOULD also apply step 2 to it. After a minter rotation, updates signed by the previous minter fail step 2 as soon as the reader holds a record signed by the new one. A new minter MUST therefore sign and serve its own updates for every comment it serves. #### Challenge exchange - `encryption.publicKey` is the raw public key of `Ms`. Publishers encrypt to it ([BSIP-3](./bsip-3.md#encryption), [BSIP-4](./bsip-4.md)). - A publisher MUST reject a `CHALLENGE` or `CHALLENGEVERIFICATION` whose signer address differs from the key address of the community's `encryption.publicKey`. - A node that has no custom `pubsubTopic` SHOULD use `Mn` as its topic. Both the topic and the encryption key change when the minter rotates, so a client MUST take them from a freshly verified record before publishing rather than from a cache. ### Setting up and rotating a minter The anchor record is signed where `As` lives and published by the node, so setup is split across the two: 1. The node generates `Ms` and `Mn` and is configured with `An` only. 2. The owner signs an anchor record with `As`: Value `/ipns/`, a long validity, and a sequence number `s`. - For the first anchor record ever published for `An`, `s` is 0. - Otherwise `s` MUST be strictly greater than every sequence previously published for `An`; an equal sequence makes the winner undefined. Because the owner is offline, the node SHOULD tell the owner which sequence to sign: the highest sequence it has accepted or can find on the network, plus a margin (RECOMMENDED: 5). If it knows neither, it MUST report that the sequence cannot be determined rather than answer 0, because a record that was never published and a lookup that failed look the same. 3. Before publishing the record, the node MUST verify that it is a valid, unexpired IPNS record for `An`, that its Value is exactly `/ipns/` followed by the node's own `Mn`, and that its sequence is strictly greater than the highest sequence the node has accepted for `An`. The node MUST NOT rely on the routing layer to reject a rollback. It then publishes the bytes exactly as signed, keeps them, and SHOULD re-provide them at every start and about once an hour while running. 4. The node SHOULD NOT publish community records before it has accepted an anchor record pointing at its minter; until then no reader can reach them. 5. Every community record the node publishes MUST carry `anchor: { "publicKey": An }`. The owner MUST re-sign the anchor record, with a higher sequence, before its validity lapses. **Rotation and revocation** are the same act on a different node: create `Mn′` there, sign `An → Mn′` with a higher sequence, and publish it. The previous node keeps `Ms`, but readers that obtain the new anchor record no longer reach it. The address, the identity key, and every stored comment stay unchanged. ### Minter rotation and key migration - A **minter rotation** keeps the identity `An`. It needs no name, readers follow it the next time they resolve `An`, and stored content stays valid. Minter-derived values change: the signer of the record and of comment updates, `encryption.publicKey`, and by default `pubsubTopic`. - A **key migration** ([BSIP-10](./bsip-10.md#key-migration)) changes the identity key. It requires a name, makes readers discard everything obtained under the old key, and is the only recovery when `As` is lost or compromised: the owner creates a new identity (for example a new anchor `An′`) and repoints the name at it. ## Rationale - **Two keys.** The owner proves once, with a key that never touches the network, which minter may publish. The minter can be replaced without the address changing. Short of threshold signatures, this is the least an offline owner can delegate. - **The IPNS chain carries the delegation.** Readers already resolve IPNS names; an anchor record that points at the minter's name reuses that machinery and the IPNS sequence rules, and lets the owner revoke by publishing one record. A delegation certificate embedded in the community record would verify offline, but revoking it would need its own expiry or revocation scheme. - **The record names its anchor.** A reader that reaches the record through the minter, for example behind a name pointing at the minter, cannot otherwise tell which identity it is looking at. The claim recovers the identity, and fetches route through the anchor afterwards. - **Claims are proven, never trusted.** The claim decides the identity, the subscription topic, and which key publications are labelled with. Accepting it unproven would let any minter present its own content under a popular community's identity and keep readers pinned to it with a future `updatedAt`. - **Content is labelled with the anchor; updates are signed by the minter.** Comments outlive minters, so they name the stable key. Comment updates and challenge messages are produced continuously by the online node, so they are signed by the key it holds. - **One hop.** A single delegation covers the offline-owner case. Each extra hop costs a sequential lookup for every reader and widens the surface for resolution abuse. - **The node tracks the sequence.** The offline owner cannot know the latest sequence; the online node can, and refusing to guess when it cannot know prevents the owner from signing a record that loses to its predecessor forever. ## Backwards Compatibility Ordinary communities are unaffected: the algorithm in [Loading and verifying a community record](#loading-and-verifying-a-community-record) reduces to the ordinary rules when the chain has one element and the record has no `anchor`. Readers that predate this BSIP cannot load delegated communities: they resolve the anchor record recursively and then reject the community record because its signer is not the anchor key. Converting an existing ordinary community into a delegated one while keeping its address is not specified here; the current implementation creates delegated communities only as new communities. ## Reference Implementation Non-normative. The current implementation is [pkc-js](https://github.com/pkcprotocol/pkc-js): chain resolution in `src/clients/base-client-manager.ts` (`resolveIpnsToCidP2P`), gateway chain validation, claim proofs, and record checks in `src/community/community-client-manager.ts`, record verification in `src/signer/signatures.ts` (`verifyCommunity`, `verifyCommentUpdate`, `verifyPageComment`), anchor record signing in `src/signer/ipns-record.ts` (`createAnchorIpnsRecord`), and the node side in `src/runtime/node/community/local-community/anchor-publishing.ts`. ## Security Considerations - **Delegate trust.** Until the owner rotates, the minter fully controls what the community publishes: it can censor, publish arbitrary records under the community's identity, set moderation state through comment updates, and read every publication sent to the community, since it holds the decryption key. Delegation limits how long a delegate holds that power, not what it can do while it holds it. - **Minter key compromise.** An attacker holding `Ms` can publish under `Mn`, including a minter record with the highest possible sequence that locks `Mn` for good, until the owner rotates to `Mn′`. After a rotation, a minter record from the old minter no longer reaches readers that obtained the new anchor record, and its comment updates are rejected once readers hold a record from the new minter. - **Anchor key compromise.** An attacker holding `As` can repoint `An` anywhere with a higher sequence; that is a full takeover of the identity. The only recovery is a key migration through the community's name ([BSIP-10](./bsip-10.md#key-migration)), so a delegated community without a name cannot recover. `As` SHOULD be kept offline and used only to sign anchor records. - **Forged anchor claims.** A minter can write any `anchor.publicKey`. A chain-walked record must claim the chain's first name, and a single-hop record's claim must be proven against the claimed anchor's own chain. A minter that is not endorsed, including a revoked minter that a stale name record still points at, has its records rejected without any value being adopted. - **Rolled-back anchor records.** The IPNS rules prefer the record with the highest sequence, but a reader that sees only one source cannot know a higher one exists. A malicious gateway or router can keep serving the previous anchor record after a rotation and pin a reader to a revoked, possibly compromised, minter. The `updatedAt` freshness check does not help, because the old minter can keep minting fresh records. Readers SHOULD persist the highest anchor-record sequence seen for each anchor and reject lower ones, and SHOULD consult more than one source. The current implementation does not yet persist it. - **Rolled-back community records.** Within a session, readers ignore records whose `updatedAt` is not newer than the one held. Across sessions, a source can serve an older validly signed minter record until that record's validity expires. - **Liveness.** An expired anchor record makes the community unreachable on every transport. The owner must re-sign it before it lapses, which is why a long validity is recommended. - **Untrusted gateways.** A gateway that recursively resolves the chain can substitute any record. Validating every IPNS record in the chain, and binding the served body's CID and signer to the terminal, leaves the gateway unable to forge anything; it can only withhold or roll back. - **Encryption-key binding.** Requiring `encryption.publicKey` to belong to the record's signer prevents a record from steering the challenge exchange, and therefore every publication's content, to a key the reader never validated against the address. - **Minter-labelled publications.** Accepting publications labelled with `Mn` would store content that stops naming the community after the next rotation. Rejecting them keeps every stored comment bound to the identity. - **Resource use.** The one-hop limit and the requirement to resolve hop by hop bound the work an attacker-controlled chain can cause, and the 10 KiB IPNS record limit bounds what a gateway can make a reader download per hop. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).