{"id":"2c4fc97a5fe1369d36c273792f54021a40015ce677157525e171ec9c4e3c2904","pubkey":"43185edecb675892824b1a37a57f3e407fbde2eda7201a3829b8cf4ba7c5b4f0","created_at":1782681143,"kind":30817,"tags":[["title","PR #2331: NIP-9B Verifiable Community Rules"],["alt","Custom NIP: PR #2331: NIP-9B Verifiable Community Rules"],["l","recreated-github-pr"],["L","org.nostr-protocol.nips"],["k","34550"],["k","34551"],["d","pr-2331-nip-9b-community-rules"]],"content":"> **Recreation note.** This is a copy of pull request [nostr-protocol/nips#2331](https://github.com/nostr-protocol/nips/pull/2331), preserved as a custom NIP because my GitHub account (`mstrofnone`) is currently suspended. The narrative below is the PR body; the spec text follows verbatim from the proposed `9B.md`.\n\n---\n\n## PR body\n\nNIP-9B: Verifiable Community Rules\n==================================\n\nAdds a new NIP defining `kind:34551` — a **machine-readable, signed rules document** for communities. Clients fetch this event before drafting a post, locally validate the draft against allowed kinds, byte limits, per-author quotas, allow/deny patterns, and required tags, and reject the send before it hits the wire.\n\nComplements rather than replaces existing moderation flows:\n\n- [NIP-72](https://github.com/nostr-protocol/nips/blob/master/72.md) communities define moderators and rely on post-hoc approval events (`kind:4550`). The community definition (`kind:34550`) already carries a freeform `rules` tag intended for human readers; this NIP adds the machine-readable companion.\n- [NIP-29](https://github.com/nostr-protocol/nips/blob/master/29.md) groups define roles and capabilities in opaque relay-side policy. This NIP exposes a portable subset of that policy to clients so they can pre-filter without round-tripping to the relay.\n\n## Why this NIP\n\nThe model is borrowed from a successful prior art: HelloZeroNet/ZeroNet's `content.json` `user_contents` mechanism, which since 2017 has shipped signed per-kind permission/quota rules to thousands of self-hosted sites. Full analysis of why this is a real gap in Nostr's moderation stack is in `nips0.txt`, summarised in the NIP body's \"Background\" section.\n\nThe rules object is **declarative** (what is allowed) and **signed by the community owner**, so any client or relay can replicate enforcement without trusting a single implementation. Untrusted relays still see only the events they receive; the win is that the *publishing* side knows up-front whether a draft will be rejected.\n\n## Scope discipline\n\n- One new event kind: `34551` (parameterised replaceable, `d` mirrors the community).\n- No changes to existing NIPs.\n- Binding to NIP-72 communities via `a` tag, to NIP-29 groups via `h` tag — both optional, both already-existing tag conventions.\n- Numbering: slot `9A` originally chosen, renumbered to `9B` after hodlbod flagged that `9A` is claimed by [#2194](https://github.com/nostr-protocol/nips/pull/2194). The two commits in this PR reflect that history; happy to squash if preferred.\n\n## Implementation evidence\n\nReference implementation:\n\n- **Client/composer side:** [vitorpamplona/amethyst#2839](https://github.com/vitorpamplona/amethyst/pull/2839) (composer-side validation) + [#2840](https://github.com/vitorpamplona/amethyst/pull/2840) (feed-side filter) + [#2798–2800](https://github.com/vitorpamplona/amethyst) (NIP-9A wiring across composer/feed)\n- **Relay enforcement / loader:** [mstrofnone/strfry-namecoin-policy](https://github.com/mstrofnone/strfry-namecoin-policy) loader + parser + validator + handler test suites (`test/nip9a-*.test.js`)\n- **Demo community:** rules JSON live at `nmcLightningService/NIP-9A-DEMO.md`, published as `kind:34551` to the standard 8-relay public set via `nmcLightningService/publish-nip9a-rules.js`.\n\nCompanion spec: see the partner PR adding a `nip9b` field to NIP-11 so relays can advertise the addressable rules document URI machine-readably.\n\n## Notes for review\n\n- No changes to existing NIPs in this PR.\n- The companion `nip9b` NIP-11 pointer is a separate small PR deliberately, to keep this one tight.\n- `kind:34551` is the next free slot in the replaceable range for moderation/policy artefacts; happy to renumber to whatever the maintainer prefers.\n\nCC: none — opening cold so the broadest set of reviewers can chime in.\n\n\n---\n\n## Proposed NIP text (`9B.md`)\n\nNIP-9B\n======\n\nVerifiable Community Rules\n--------------------------\n\n`draft` `optional`\n\nThis NIP defines an addressable event that lets a community owner publish a\n**machine-readable, signed rules document** for a community. Clients can fetch\nthis event before submitting a post and reject the draft locally if it would\nviolate any rule, surfacing the violation to the user before send.\n\nThis complements rather than replaces existing moderation flows:\n\n- [NIP-72](72.md) communities define moderators and rely on post-hoc approval\n  events (`kind:4550`). The community definition (`kind:34550`) already carries\n  a freeform `rules` tag intended for human readers; this NIP adds the\n  machine-readable companion.\n- [NIP-29](29.md) groups define roles and capabilities in opaque relay-side\n  policy. This NIP exposes a portable subset of that policy to clients.\n\nThe rules object is **declarative** (what is allowed) and signed by the\ncommunity owner, so any client or relay can replicate enforcement without\ntrusting a single implementation.\n\n## Event definition\n\nThe rules document is a parameterised replaceable event of `kind:34551`,\naddressable by the same `d` tag as the community it governs.\n\n```jsonc\n{\n  \"kind\": 34551,\n  \"pubkey\": \"<community-owner-pubkey>\",\n  \"tags\": [\n    // Identity of this rules document. Mirrors the d-tag of the community.\n    [\"d\", \"<community-d-identifier>\"],\n\n    // Bind to the community this rules document governs.\n    // For NIP-72: a 34550 address pointer.\n    [\"a\", \"34550:<community-author-pubkey>:<community-d-identifier>\"],\n    // For NIP-29: alternative binding by host'group-id (see \"Bindings\" below).\n    // [\"h\", \"<host>'<group-id>\"],\n\n    // Allowed event kinds. May appear multiple times.\n    // Form: [\"k\", \"<kind>\", \"<max-bytes?>\", \"<max-per-author-per-day?>\"]\n    [\"k\", \"1111\", \"16384\", \"50\"],\n    [\"k\", \"20\",   \"262144\", \"10\"],\n    [\"k\", \"30023\", \"262144\", \"5\"],\n\n    // Per-pubkey policy overrides. May appear multiple times.\n    // Form: [\"p\", \"<hex>\", \"<allow|deny>\", \"<role?>\"]\n    [\"p\", \"<trusted-pubkey>\", \"allow\", \"contributor\"],\n    [\"p\", \"<bad-actor-pubkey>\", \"deny\"],\n\n    // Optional web-of-trust gate: posts only allowed if the author is reachable\n    // from <root-pubkey> through follow lists within <depth> hops.\n    [\"wot\", \"<root-pubkey>\", \"2\"],\n\n    // Hard size cap independent of kind (defence in depth).\n    [\"max_event_size\", \"524288\"],\n\n    // Anti-rollback ratchet: clients MUST refuse rules events whose\n    // `created_at` is less than this value, even if signed by the owner.\n    [\"min_rules_created_at\", \"1746604800\"]\n  ],\n  \"content\": \"\"\n}\n```\n\nTag semantics:\n\n- **`d`** — REQUIRED. Identifier of this rules document. SHOULD match the\n  community's own `d` tag for clarity.\n- **`a`** or **`h`** — REQUIRED, exactly one. Binds this rules document to a\n  specific community. `a` for NIP-72 (`34550:<pubkey>:<d>`); `h` for NIP-29\n  (`<host>'<group-id>` per NIP-29).\n- **`k`** — REQUIRED, one or more. Each occurrence whitelists one event kind\n  for the community. Position 2 is the kind (decimal string). Position 3 is\n  optional `max-bytes` (size of the JSON-encoded event); absent or empty means\n  \"no kind-specific limit\". Position 4 is optional\n  `max-per-author-per-day` quota; absent or empty means \"no quota\". Events\n  whose kind has no `k` entry MUST be rejected.\n- **`p`** — OPTIONAL, zero or more. Position 2 is the pubkey hex. Position 3\n  is `\"allow\"` or `\"deny\"`. Position 4 is an optional role label (free-form,\n  not normative). `deny` overrides any `allow` and any other rule.\n- **`wot`** — OPTIONAL, zero or one. Position 2 is the root pubkey of the\n  trust graph. Position 3 is the maximum follow-graph depth (positive\n  integer). The lookup mechanism is unspecified; clients MAY use NIP-02\n  follow lists, NIP-85 trusted assertions, or any locally available source.\n  Multiple `wot` tags MUST be treated as an OR (any one passing is enough).\n- **`max_event_size`** — OPTIONAL, zero or one. Hard cap on the JSON-encoded\n  event byte size, applied in addition to any `k`-tag size limit.\n- **`min_rules_created_at`** — OPTIONAL, zero or one. A monotonic ratchet:\n  clients MUST ignore any `34551` event from this owner whose `created_at`\n  is below this value, including older versions of the rules themselves\n  found on stale relays.\n\n## Behaviour\n\n### Owner\n\n- Only the community owner (the same pubkey that signs `kind:34550` for\n  NIP-72, or the relay master key for NIP-29) MAY publish a `34551` event.\n  Events signed by anyone else MUST be ignored by clients and SHOULD be\n  rejected by relays that recognise this kind.\n- Owners SHOULD increment `min_rules_created_at` whenever they intentionally\n  tighten rules, to defend against an attacker replaying an older laxer\n  version.\n\n### Client (composer)\n\nBefore publishing any event into a community, clients SHOULD:\n\n1. Fetch the latest `34551` event for the community.\n2. Locally validate the draft event against the rules.\n3. If the draft would be rejected, surface the specific violation to the user\n   before sending (e.g. \"this kind is not allowed in this community\", \"your\n   post is over the 16 KB limit\", \"you have hit your 50 posts/day quota\").\n4. Either block the send entirely or require explicit user confirmation,\n   per client policy.\n\nA client MAY proceed with sending if no `34551` is found for the community.\nAbsence of rules MUST NOT be treated as deny-by-default.\n\n### Client (reader)\n\nClients displaying a community feed MAY filter out events that violate the\nrules document, in addition to any NIP-72 approval-based filtering. This is\nuseful for displaying NIP-72 communities where moderators have not yet\nissued `4550` approvals.\n\n### Relay\n\nRelays MAY enforce the rules server-side by rejecting `EVENT` submissions\nthat violate the latest `34551` for the community. This is OPTIONAL: the\nrules are signed and verifiable, so honest enforcement can occur at any\nlayer. Relays SHOULD NOT silently strip non-conformant events.\n\n## Security considerations\n\n### Stolen owner key\n\nA stolen owner key can publish maximally permissive rules. The\n`min_rules_created_at` ratchet limits the damage window only to the period\nbetween key compromise and detection; once detected, the legitimate owner\nSHOULD rotate the underlying community pubkey (NIP-72 community owners can\ndo this by publishing a new `34550` from a new key — there is no recovery\npath for the original community identity).\n\n### Stale rules on dead relays\n\nReplaceable events can linger on relays that have stopped receiving\nupdates. The `min_rules_created_at` ratchet is the primary defence: once\nthe owner ratchets it forward, clients are required to reject any older\nversion. Clients SHOULD prefer rules from relays the community itself\nadvertises (NIP-72 `relay` tag with `requests` or `approvals` markers).\n\n### Rule-driven content blocking\n\nA malicious owner could weaponise per-pubkey `deny` to silence dissent\nwithin their community. This is a property of any owner-controlled forum\nand not specific to this NIP. NIP-72 already permits this by virtue of the\nmoderator list. Clients SHOULD make the rules document visible to readers\nso members can see the deny-list and choose to leave.\n\n### WoT denial of service\n\nThe `wot` tag asks clients to traverse a follow graph at submission time.\nClients SHOULD cap the cost of this traversal locally and SHOULD cache the\nresult. Relays enforcing `wot` server-side MUST cap traversal cost or risk\namplification attacks via crafted communities.\n\n## Mapping from ZeroNet `permission_rules`\n\nThis NIP is informed by ZeroNet's `content.json` `user_contents` feature.\nFor implementers familiar with ZeroNet:\n\n| ZeroNet                                                | This NIP                       |\n| ------------------------------------------------------ | ------------------------------ |\n| `permission_rules.\".*\".max_size`                       | `max_event_size` tag           |\n| `permission_rules.\".*\".files_allowed`                  | `k` tag list                   |\n| `permission_rules.\"bitid/.*@zeroid.bit\".max_size`      | (deferred — see Open Issues)   |\n| `permissions.\"bad_user@zeroid.bit\": false`             | `p` tag with `deny`            |\n| `cert_signers`                                         | `wot` tag                      |\n\n## Open issues\n\n- **Per-author-pattern overrides.** ZeroNet uses regex patterns over\n  `bitid/<id>@<cert-issuer>`. Nostr identities are flat pubkeys; the\n  closest equivalent is a per-pubkey `p` tag, which does not scale to\n  large allowlists. A future revision could add `[\"p_size\", \"<hex>\",\n  \"<max-bytes>\"]` for per-author size overrides, but v1 keeps the schema\n  small.\n- **Replaceable events with non-trivial diffs.** When a user edits a\n  long-form post (`kind:30023`), the new version may pass rules while\n  the old version did not (or vice-versa). v1 leaves this to client\n  policy; future revisions could specify \"rules apply at original\n  publication time of the address\".\n- **NIP-29 binding.** The `h` tag form binds to a NIP-29 group, but the\n  signing pubkey for NIP-29 metadata is the *relay* master key, not a\n  user. Reviewers please confirm whether `34551` from the relay master\n  key for an `h`-bound community is the right ergonomics, or whether\n  NIP-29 should use a different kind entirely.\n","sig":"1ec568c5d9b178867b108187739268e053e8ff010cc7ed18e9c6f10bc6e74b654fb162ab150ba110ee9ba414bd87bd81e44acc55bdb225a8e37fbfc67e6fcb47"}