On this page
- Abstract
- Motivation
- Specification
- Overview
- Common publication fields
- Author-set comment fields
- Flairs
- Posts and replies
- Authors
- Community addressing
- The comment record
- Reserved names
- Addressing and fetching
- Pseudonymity
- Community acceptance of comments
- Signatures
- Rationale
- Backwards Compatibility
- Security Considerations
- Copyright
Abstract
A comment is the basic unit of user-authored content in Bitsocial. Posts and replies are both comments: an object signed by an author and published to a community. Once accepted by a community, a comment is stored as an immutable record addressed by its IPFS CID. This BSIP defines the comment format, the fields every publication shares, how comments are addressed and fetched, how posts and replies relate, how a community may pseudonymize authors, and the signature rules that every signed Bitsocial record follows.
Motivation
Independent Bitsocial clients must agree on exactly what a comment is — which fields exist, which are author-set, which are community-set, which bytes are signed, and how its address is computed — or they cannot display, validate, or relay each other’s content. This BSIP is the canonical description of that shared format. Its Signatures section is also the signing scheme the other Core BSIPs reference.
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 and RFC 8174.
Overview
A comment exists in three forms:
- The comment publication: the object the author signs and sends, encrypted, to the community in a challenge exchange (BSIP-4). It exists only during publishing.
- The comment record: the publication exactly as signed, plus a few fields the community sets (The comment record). The community adds it to IPFS once it accepts the publication; from then on it is immutable and addressed by its CID.
- The comment update: the mutable state of the comment (votes, replies, moderation), signed by the community and specified in BSIP-5.
Clients display a comment by combining its record with its latest comment update.
Common publication fields
Every publication type — comments, votes (BSIP-6), comment edits (BSIP-7), comment moderations (BSIP-8), and community edits (BSIP-9) — carries these fields:
timestamp— REQUIRED. Positive integer Unix time in seconds.protocolVersion— REQUIRED. Non-empty string, currently"1.0.0".signature— REQUIRED. The publisher’s signature: the author’s, or for a moderation or community edit, the role holder’s (see Signatures).author— OPTIONAL. The author object (see Authors). When the author has no fields to publish,authorMUST be omitted; it MUST NOT be an empty object{}.communityPublicKeyandcommunityName— the community the publication is addressed to (see Community addressing).
Fields that only exist inside a client — the signing key (signer), the challenge options passed
alongside a publication (challengeRequest), and derived convenience values such as
communityAddress, address, publicKey, shortAddress, shortCid, and nameResolved — are never
signed and MUST NOT appear on the wire. A publication MUST NOT carry a field its type reserves for
another purpose (see Reserved names); communities reject such publications.
Author-set comment fields
A comment publication is a JSON object with the common publication fields
and the following content fields. The author MUST set at least one of title, content, link,
crosspost, or gallery.
title— string, the post title.content— string, the body text (Markdown).link— string URL, 1 to 2000 characters.linkWidth,linkHeight— positive numbers, the media dimensions oflink, when known.linkHtmlTagName— non-empty string, a hint for how to embedlink(for exampleimg,video, oraudio).flairs— array of flair objects chosen by the author for the post.spoiler— boolean.nsfw— boolean.parentCid— CID of the parent comment. Present on replies, absent on posts.postCid— CID of the root post of the thread. REQUIRED whenparentCidis set; MUST be absent on posts.quotedCids— array of CIDs of comments this reply quotes. Only replies may carry it, and every quoted comment MUST belong to the same thread.crosspost— a repost of another comment record, embedded verbatim. Its shape and verification are specified in BSIP-12. A comment whose only payload iscrosspostis valid.gallery— a non-empty array of media items (images, videos, or audio). Each item is an object withurl(string, 1 to 2000 characters),htmlTagName(img,video, oraudio), and optionalwidthandheight(positive numbers) andalt(string of at most 1000 characters, the author’s text description of the media). Items MAY carry further members, which are signed with the comment and MUST be preserved. A comment without a gallery omits the field; an empty array is invalid. A comment whose only payload isgalleryis valid. A comment MAY carry bothlinkandgallery; clients show thelinkfirst, then the gallery. Readers check only this structure; the media checks in Community acceptance of comments apply when a community accepts the comment, so a later change to them cannot invalidate a published one.
Example comment publication:
{
"title": "Why did the banana go to the doctor?",
"content": "It wasn't peeling well.",
"communityPublicKey": "12D3KooWBexQF4qDvyaxDgK4r2qPNk5z8s9b1eU5z2bnR6dExamPLe",
"communityName": "jokes.bso",
"author": { "name": "john.bso", "displayName": "John" },
"timestamp": 1728174027,
"protocolVersion": "1.0.0",
"signature": {
"type": "ed25519",
"signature": "<base64>",
"publicKey": "<base64, 32 bytes>",
"signedPropertyNames": ["content", "title", "author", "communityPublicKey", "communityName", "protocolVersion", "timestamp"]
}
}
Flairs
A flair is an object with a required text string and optional backgroundColor and textColor
strings and expiresAt (integer Unix time in seconds). Additional fields are allowed. Flairs appear
on comments (flairs), on authors (author.flairs), in comment updates set by moderators
(BSIP-5), and in the community record’s list of allowed flairs
(BSIP-3). A community accepts author-chosen flairs only when its features allow them
and each flair is deep-equal to one the community lists (see
Community acceptance of comments).
Posts and replies
depth distinguishes posts from replies and is set on the comment record by the community, not by
the author:
- A post has
depth0, noparentCid, and nopostCid. - A top-level reply has
depth1, withparentCidandpostCidboth pointing at the post. - A nested reply has
depth2 or greater, withparentCidpointing at its immediate parent andpostCidpointing at the root post. Itsdepthis its parent’sdepthplus one.
Authors
The author object describes the author. All of its fields are OPTIONAL:
name— the author’s domain name, for examplejohn.bso. It MUST be a domain name (see BSIP-10) written in lowercase; a free-form name belongs indisplayName.displayName— a free-form display name.previousCommentCid— CID of the author’s previous comment, in any community, forming a per-author chain the author maintains.wallets— a record keyed by chain ticker, each value{ "address": string, "timestamp": integer, "signature": { "signature": string, "type": string } }proving the author controls that address.avatar— an NFT avatar{ "chainTicker": string, "address": string, "id": string, "timestamp": integer, "signature": { "signature": string, "type": string } }.flairs— array of flair objects.
Other fields MAY be present and are signed as part of author, except names reserved for derived
values: address, publicKey, shortAddress, community, and nameResolved MUST NOT appear in a
published author. The proof formats inside wallets and avatar are verified independently of the
publication signature and are not yet specified by a BSIP.
An author’s cryptographic identity is the signature.publicKey of their publication. Clients derive
the author address as author.name || publicKey, where publicKey is the IPNS name (a libp2p
PeerId string such as 12D3KooW…) of the signing key (BSIP-10). The
address is a runtime value and is not part of the wire object, and it is not a verdict: whether
author.name actually belongs to the signing key is a separate result of verifying the name
(BSIP-10). A client MUST NOT present author.name as the
author’s verified identity unless that verification succeeded, and MUST show the key-derived address
when it did not.
The comment update carries the community’s view of the author (karma, ban state, moderator flairs) in
author.community (BSIP-5); that object is never part of a
publication.
Community addressing
A publication names the community it is published to with two fields:
communityPublicKey— the IPNS name (PeerId string) of the community’s identity key, as defined in BSIP-3. For a delegated community this is the anchor key, not the key that signs the community record (BSIP-11). REQUIRED on new publications.communityName— the community’s domain name, for examplejokes.bso. REQUIRED when the community has a name, and then it MUST be exactly that name; MUST be absent when the community has none (BSIP-10).
A community rejects a publication whose communityPublicKey is missing or differs from its identity
key, or whose communityName differs from its name, or that omits communityName when the community
has one. Clients MAY expose a convenience communityAddress equal to communityName when present,
otherwise communityPublicKey. communityAddress is a runtime value only: it MUST NOT appear on the
wire and MUST NOT be signed.
The comment record
When a community accepts a comment publication, it stores the comment record: the publication with every author-signed field preserved byte for byte, plus the following community-set fields. They are not covered by the author’s signature and MUST be treated as community-asserted metadata:
depth— REQUIRED. Non-negative integer, as described in Posts and replies.previousCid— OPTIONAL. A link to an earlier comment in the same list: for a post, the CID of the most recent post the community had accepted before it; for a reply, the CID of an earlier reply to the same parent. It is a traversal hint; clients MUST NOT treat it as an authoritative ordering.thumbnailUrl(non-empty string),thumbnailUrlWidth,thumbnailUrlHeight(positive numbers) — OPTIONAL. A thumbnail the community generated forlink.pseudonymityMode— OPTIONAL. One ofper-post,per-reply, orper-author, present when the community pseudonymized the author of this comment (see Pseudonymity).
A record MUST NOT carry any other community-set field. Mutable counts, scores, replies, and moderation state are not part of the comment record; they live in the comment update (BSIP-5).
Because the record is the publication byte for byte, a community MUST refuse a publication it could
not store and republish exactly, including one with an empty author object or with a string that is
not well-formed Unicode (for example a lone UTF-16 surrogate), since either would change the signed
bytes or the CID once stored.
Reserved names
Some names are reserved because they mean something else on another record or in implementations, and accepting them on the wire would let a record smuggle state into a client. A comment record, and a comment publication, MUST NOT carry a top-level member named:
- a comment update field that is not also a comment field:
cid,upvoteCount,downvoteCount,replyCount,childCount,number,postNumber,edit,pinned,locked,archived,removed,reason,approved,updatedAt,lastChildCid,lastReplyTimestamp,replies, orpendingApproval; - a runtime or transport name:
signer,challengeRequest,communityAddress,challengeAnswers,challengeCommentCids,comment,commentUpdate,original,shortCid,shortCommunityAddress,deleted,raw,state,clients,publishingState,updatingState,nameResolved, orabortSignal; - a storage name used by the current implementation:
insertedAt,authorSignerAddress,originalCommentSignatureEncoded,extraProps,challengeCommentUpdate,postUpdatesBucket,publishedToPostUpdatesMFS, orrowid.
A comment publication additionally MUST NOT carry the community-set fields of
the comment record. The reserved names inside author are listed in
Authors. Clients MUST reject a fetched comment record that carries a reserved name.
Addressing and fetching
A comment is addressed by the CID of its record. The CID is computed as follows:
- Serialize the comment record as JSON with no insignificant whitespace, with the keys of every
object, at every depth, sorted in ascending order of their UTF-16 code units. Strings are escaped
as ECMAScript
JSON.stringifyescapes them:",\, and U+0000–U+001F are escaped (using\b,\f,\n,\r,\twhere they apply and\u00xxotherwise), lone surrogates are written as\udxxx, and every other character is written as itself. Numbers are written as ECMAScript’sString(number)writes them. Array elements keep their order. - Encode the resulting string as UTF-8.
- Import those bytes as IPFS does by default without raw leaves: a UnixFS file in dag-pb, in
262,144-byte chunks with a balanced layout of at most 174 children per node, hashed with sha2-256,
and expressed as a CIDv0 (
Qm…). For a record ofn≤ 262,144 bytesb, this is the base58btc encoding of0x12 0x20followed by the sha2-256 of the block0x0a ‖ varint(m) ‖ 0x08 0x02 0x12 ‖ varint(n) ‖ b ‖ 0x18 ‖ varint(n), wheremis the length of everything aftervarint(m).
The comment record is fetched by its CID from the peer-to-peer network, or from an IPFS gateway at
https://<gateway>/ipfs/<cid>. A client MUST check that a fetched record hashes to the CID it
requested, and MUST verify its signature as described below; the CID proves integrity, not
authorship. Test vectors for this computation are in
assets/bsip-2/cid-vectors.json.
Pseudonymity
A community MAY set features.pseudonymityMode in its record (BSIP-3) to hide the
long-lived identities of its authors from other users. When it does, for every accepted comment whose
author does not hold the owner, admin, or moderator role, the community:
- replaces
authorwith an object containing only the author’sdisplayName, or omitsauthorwhen there is none; - re-signs the comment, following Signatures, with an alias key it generates and
keeps privately:
per-reply— a fresh alias for every comment;per-post— one alias per author per thread: a new post gets a fresh alias, and the author’s replies in that thread reuse it;per-author— one alias per author for the whole community;
- sets
pseudonymityModeon the stored record.
The comment record then carries the alias’s signature, and clients verify it like any other record. Author edits of a pseudonymized comment are authorized against the original author and re-signed with the same alias (BSIP-7). Pseudonymity hides an author from other users only: the community node knows the mapping between aliases and original authors.
Community acceptance of comments
In addition to the rules every publication must meet (BSIP-4), a community MUST reject a comment publication that:
- sets none of
title,content,link,crosspost, orgallery; - sets
parentCidwithoutpostCid, or whosepostCidis not the root post ofparentCid’s thread; - sets
quotedCidson a post, or quotes a comment that does not exist in the community, belongs to another thread, or is pending approval; - carries a
crosspostthe community does not accept (BSIP-12); - carries a
gallerywhen the community’sfeatures.maxPostGalleryItems(for a post) orfeatures.maxReplyGalleryItems(for a reply) is absent or 0, or with more items than that maximum (BSIP-3); - carries a
galleryitem whoseurlis not anhttpsURL whose path ends in a file extension of an image, video, or audio type, or whosehtmlTagNamedoes not match that type:imgfor images (animated ones included),videofor video,audiofor audio; - has the same signature as a comment it already stored (a duplicate);
- carries author-chosen post
flairswhenfeatures.postFlairsis not enabled, or a post flair not listed in the record’sflairs.post; orauthor.flairswhenfeatures.authorFlairsis not enabled, or an author flair not listed inflairs.author(author flairs are not checked when the community pseudonymizes authors); - violates an enabled feature toggle in the community record’s
features(BSIP-3), for example a link or media restriction, a spoiler restriction, markdown media restrictions, required flairs, ornoNestedReplies.
Signatures
Every signed Bitsocial record — publications, comment records, comment updates, community records,
and challenge-exchange messages — carries a signature object:
{
"type": "ed25519",
"signature": "<base64, 64 bytes>",
"publicKey": "<base64, 32 bytes>",
"signedPropertyNames": ["...the names of the signed fields..."]
}
type MUST be "ed25519", the only signature type currently defined; verifiers SHOULD reject other
types. Records stored on IPFS carry signature and publicKey as base64 strings in the standard
alphabet without = padding (86 and 43 characters); since implementations compare public keys
as strings, padded values MUST NOT be produced. Challenge-exchange messages carry them as CBOR byte
strings (BSIP-4).
To sign a record, the signer first removes, at every depth, object properties whose value is null,
absent, or an empty object (array elements are left as they are), and sets signedPropertyNames to
the names of the record’s remaining signable fields. The signed message is then built as follows, by
signer and verifier alike:
- Take the record’s properties whose names appear in
signedPropertyNames, in any order. - Drop every such property whose value is
nullor absent. A verifier MUST accept a name listed insignedPropertyNamesthat the record does not carry. - Encode the remaining properties as a CBOR map (RFC 8949)
using deterministic encoding, as cborg’s default encoder does:
- every map, at every depth, has its keys sorted shorter-first and then bytewise, which for the text keys used here is the core deterministic order of RFC 8949, section 4.2.1;
- lengths are definite and no tags are used;
- integers within ±(2⁵³ − 1) use the shortest integer encoding, and
-0encodes as0; - other numbers use the shortest of float16, float32, or float64 that holds the value exactly;
- strings are text strings, booleans are
true/false, a nestednullis CBORnull, arrays keep their order, and byte strings (only in challenge-exchange messages) are byte strings; - an
undefinedvalue cannot be encoded, so a record carrying one cannot be signed or verified.
- Sign the bytes with Ed25519 (RFC 8032) using the key
whose public half is
publicKey. Verifiers SHOULD apply the ZIP-215 validation rules, as the reference implementation does, so that every implementation accepts exactly the same signatures.
The signature field itself is never listed in signedPropertyNames. Verification re-derives the
signed bytes from the signedPropertyNames carried in the record — never from a hardcoded list —
and checks the signature against publicKey. This keeps records signed under older field sets
verifiable.
A verifier MUST reject a record that carries a field not listed in its signedPropertyNames, with
one exception: on a comment record, the community-set fields and fields
unknown to the verifier MAY be present unsigned. A verifier MUST still reject a comment record that
carries a known author-signable field (any field from
Author-set comment fields or the
common publication fields) that is not listed in signedPropertyNames.
Unsigned fields MUST NOT be presented as authored by the signer.
Test vectors for these rules, including the CBOR bytes each signature covers and negative cases,
are in assets/bsip-2/signature-vectors.json.
Rationale
- CBOR over a list of named fields gives a canonical, language-independent byte string to sign,
and lets the protocol evolve: new optional fields can be added without breaking verification of
older records, because verifiers trust each record’s own
signedPropertyNames. - Rejecting unsigned fields closes the gap a relay could otherwise use to append forged properties; the comment record’s community-set fields are the one deliberate, named exception.
- Splitting author-set and community-set fields keeps the author’s signature meaningful: a
community can attach
depthor a thumbnail without being able to forge author content. - Storing the publication byte for byte keeps the record’s CID and signature stable through the community’s storage and every later page that embeds it.
- Separating mutable state into comment updates (BSIP-5) lets the immutable, CID-addressed comment stay stable while votes and replies accumulate.
Backwards Compatibility
Older records carry the community address as a single signed field, subplebbitAddress, rather than
as separate communityPublicKey and communityName, and older authors carry a signed
author.address. An implementation that reads such records SHOULD treat a value in domain form (for
example ending in .bso or .eth) as the name and any other value as the public key, and MUST NOT
require communityPublicKey on records stored before it was introduced. Communities MUST reject new
publications that carry subplebbitAddress or communityAddress. Because verification uses each
record’s own signedPropertyNames, records signed with a different set of fields remain verifiable.
Security Considerations
- Unsigned fields. A relaying peer can append arbitrary properties to a record in transit. Verifiers MUST reject records with unsigned fields as specified above and MUST NOT act on unsigned data as if the author had signed it.
- Name spoofing.
author.nameandcommunityNameare human-readable claims. They MUST be verified against the corresponding public key (BSIP-10) before display as verified; otherwise an attacker could publish under a name they do not control. - Content addressing. A comment fetched by CID is integrity-protected by the hash, but the CID does not prove authorship — the signature does. Always verify both.
- Pseudonymity limits. Pseudonymity protects authors from other users, not from the community operator, who holds the alias mapping. Other metadata (writing style, timing, links) can still correlate an author’s comments.
- Link handling.
link,linkHtmlTagName, and thumbnails describe remote media. Clients SHOULD treat them as untrusted input and apply their own embedding, sanitization, and privacy policy; fetching remote media can reveal the reader’s IP address to its host. - Non-canonical strings. Strings that are not well-formed Unicode can encode differently in different implementations, breaking signature or CID verification after storage; communities reject them for that reason.
Copyright
Copyright and related rights waived via CC0.