NIP-29: add subgroups spec#2319
Conversation
There was a problem hiding this comment.
The deletion behavior is going to be confusing for users who use a client that doesn't have subgroups (because they don't see the hierarchy), and won't be actionable for them. I would prefer orphan sub groups to this (and cascading, yeesh). If parent is invalid, the group is not a sub-group. This is a case clients will have to cover anyway, because they might not have a complete list of group events at any given time.
I also don't think implicit membership inheritance is a good idea. I'm working on this here at a relay level, which has the same problem. Policy inheritance should be explicit — since the relay is the one enforcing the policy, if it wants to cascade membership it should update sub-groups to match parent groups instead of expecting clients to infer it (which clients that don't support sub-groups won't).
I think this PR is too complex (just because of the nature of the problem), and something more laissez faire would be better. For example:
39000 events MAY include a `parent` tag. Clients MAY display groups in a hierarchy. Relays MAY implement whatever policy they want for nested groups.
|
That makes sense @staab, I'll simplify it. |
403d577 to
fcfac59
Compare
|
I didn't read the original proposal, but I like the way it is now. |
|
Another point is how to prevent groups from becoming children without the parent group's permission. Any ideas, @staab, @fiatjaf? Something like the following might solve the problem, what do you think?
|
staab
left a comment
There was a problem hiding this comment.
Much better.
how to identify if the relay supports subgroups
I don't know that it should. NIP 29 in the supported_nips field should be enough; relays that don't support sub-groups should ignore instead of reject parent tags. Required logic should be handled by clients; relays can augment them with additional policy. This is the only way to get full compatibility. Which means this NIP has to fully define parent tag semantics when relays don't support it at all.
Another point is how to prevent groups from becoming children without the parent group's permission.
You're right, this is annoying. Probably leave this up to the relay. I could see this being handled in a few ways:
- The relay auto-updates the child group's permissions/ownership to match the parent. The original sub-group admin would be basically donating its group to the parent's admin.
- Leave permissions alone, and just live with weird hierarchies. Probably bad.
- Don't allow moving groups; force delete/re-create. Only group admins can create child groups.
None of these is very satisfying to me.
|
|
||
| A group without a `parent` tag is a root group. To build the tree, clients fetch `{"kinds":[39000]}` and assemble it locally: events without a `parent` tag are roots; the rest are grouped under the `d` referenced by their `parent` tag. | ||
|
|
||
| Subgroups are self-advertising for readers: if a relay serves `kind:39000` events with `parent` tags, it supports them; otherwise it doesn't, and there is nothing to render hierarchically. Admins creating or re-parenting subgroups probe by attempting the `kind:9002` event — a relay that does not implement subgroups rejects it via the `OK` response. |
There was a problem hiding this comment.
Admins creating or re-parenting subgroups probe by attempting the
kind:9002event — a relay that does not implement subgroups rejects it via theOKresponse.
I don't think you can rely on relays not supporting this part of the spec to detect/reject this.
| ### Rules | ||
|
|
||
| - A `kind:39000` MUST have at most one `parent` tag. An empty value (`["parent"]` or `["parent", ""]`) is equivalent to the tag being absent and indicates a root group. | ||
| - Relays MUST reject any `kind:39000` whose `parent` tag would create a cycle, including self-reference. |
There was a problem hiding this comment.
Probably SHOULD, current implementations won't do this.
|
@staab @Anderson-Juhasc Was reading through the thread, had a few suggestions;
|
luthen-seas
left a comment
There was a problem hiding this comment.
Spec text looks sound and backwards-compat is clean — zero new kinds, four new tag names (parent, child, closed-children, open-children) all currently unused elsewhere in the registry. The v2 simplification (orphan handling delegated to clients, support self-advertised via observed parent tags) is the right call.
Three small non-blocking suggestions:
1. Replacement-on-touch on child is a real footgun. "If any child tag is present, the relay SHOULD replace the parent's full child list" — a client adding one child must re-send every existing one or silently drop them. This is materially different from how clients usually treat tags. Worth an explicit implementation note plus a worked "add-one-child" example so tooling authors don't get bitten.
2. ["closed-children"] with no child tags = "accepts no children" is correct but easy to misread. A reader can plausibly parse it as "the list is sealed but existing entries remain legitimate," which is the opposite of the intended semantics. A short worked example of a parent that refuses all children would remove the ambiguity.
3. Cross-relay tiebreaker is circular. "When aggregating across relays … the kind:39000 from the relay currently serving the group's events is authoritative" — but a client in discovery mode usually doesn't know which relay is "currently serving." Suggest a deterministic fallback: prefer the most recent created_at for the same d, ties broken by the user's relay preference order from kind:10002.
One nit: parent is a generic token in a global tag namespace. NIP-29's h-scoping contains the collision risk, but a more specific name (e.g. g or parent-h) would future-proof against another NIP reaching for parent in a different domain. Acceptable as-is.
On interop: the subgroups feature is unimplemented today since it's brand new, and broader NIP-29 support outside of pippellia-btc/relay29 is still thin across the libraries and relays I've looked at. Worth tracking adoption alongside merge.
|
@luthen-seas I implemented it in this relay: fiatjaf/relay29#21 and in this client https://web.nostrord.com/ behind an experimental toggle. |
luthen-seas
left a comment
There was a problem hiding this comment.
Thanks @Anderson-Juhasc — that updates my interop point. The relay29 PR's integration suite khatru29/subgroups_test.go covers the parts I'd most want a reference implementation to nail down: cycle rejection at both RejectEvent and on startup replay against DB-planted rogue events, the replacement-on-touch child semantics (including the bare ["child"] clear marker), admin attestation round-trip, and parent-deletion → child promotion. With that plus the nostrord client behind a flag, the "≥2 client + ≥1 relay" framing is on much firmer ground than I gave it credit for.
Will keep an eye on the thread.
|
Removed some text, there was too much. Rewrote some of the AI output to be more succinct. And made the "child" tags mandatory and enforced by the relay. Looks good to me now. |
|
Excellent, well-crafted @fiatjaf! |
1fcaafa to
a07426e
Compare
Introduces hierarchical subgroups via a `parent` tag on `kind:39000`, a `kind:39005` parent manifest for discovery, opt-in membership inheritance, cascade deletion, and reconciliation rules. Also clarifies that group existence is authoritative via `kind:39000` only, since moderation events persist in history after deletion.
- Behavioral relay support detection via probing (kind:9002 with parent tag) - Bilateral declaration: parent may list accepted children via ["child", "<id>"] tags, with optional ["closed-children"] flag for strict acceptance - Admin attestation: relay may copy kind:9002 author's pubkey into the parent tag's 3rd element; clients verify it against the parent's kind:39001 admins (Matrix-inspired power-level pattern) - Client classification: confirmed / unverified / invalid - Cross-relay scope, attestation revocation, and role inheritance clarified - Optional polish on child tag: <order> (sibling ordering) and "suggested" flag
…rship independence) - advertise subgroup support via nip29.subgroups in NIP-11, drop the write-based probe; keep passive detection via observed parent tags - move at-most-one-parent and cycle checks onto the kind:9002 (the user-published event) instead of the relay-emitted kind:39000 - reference kind:9008 for parent deletion - state re-parenting needs no consent from the previous parent - document the child list as a top-down traversal hint (parent is not filterable) and not an authoritative list - make membership strictly independent: no cascade, no inheritance
a07426e to
692e295
Compare
|
I'm happy with this. |
|
I really don't understand why you use the word "reparenting" here so much. Is this a term an LLM came up with? I don't know what it means. Is it for when you have a child but then you don't have anymore and then you adopt that child again? I don't see why this should be in the spec. "adopting" would be enough and more generic. Also this last commit breaks things again. If you want to remove a parent you should just send an "edit-metadata" without any parent (since the "edit-metadata" overwrites all the group metadata), the |
e756144 to
692e295
Compare
|
I understood. “Reparenting” was a term suggested by LLM. I was trying to avoid sending all the tags to make the edits. But since kind:9002 includes all tags it doesn't make sense. |
|
Yeah, initially |
edit-metadata overwrites all group metadata, so removing a parent is just an edit-metadata that omits the parent tag. Replace the ambiguous ["parent", ""] in the example, and re-send name to show the overwrite. Reword the "re-parent" comment to "adopt". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pported Per nostr-protocol/nips#2319 a relay advertises subgroup support with a top-level nip29:{subgroups:true} in its NIP-11 document. Parse it into Nip11RelayInfo.supportsSubgroups (null/false = unsupported). The group sidebar now shows the subgroups section + add button only when the hosting relay supports them; otherwise it shows "This relay doesn't support subgroups." (web + Compose). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add hierarchical subgroups per nostr-protocol/nips#2319. No new event kinds; the spec adds parent/child tag names on kind:39000 and a NIP-11 nip29.subgroups capability flag. NMP is a client/protocol crate, so this implements the read side (projections) + publish side (actions) + NIP-11 detection; relay-enforced rules (cycle rejection, bilateral consent, child-list auto-sync) remain the relay's job. Read side: - DiscoveredGroup + JoinedGroup gain parent: Option<String> and children: Vec<String> (ordered), folded from kind:39000 via a shared kinds::tags helper (one canonical parent/child extractor). - discovered_groups.fbs + joined_groups.fbs bump schema_version 1->2; Rust + Swift FlatBuffers bindings regenerated (flatc 25.12.19). - PROJECTION_CONTRACT versions bumped 1->2; producer consts regenerated. Publish side: - CreatePublicGroupInput gains optional parent (emits ["parent", id] on the kind:9002 metadata edit). - New SetParent action (nmp.nip29.set_parent) for adopt/detach, emitting a kind:9002 with only the parent tag (relay retains prior metadata). - Shared metadata_tags::metadata_edit_tags builder is the single canonical kind:9002 authoring path (no fragmentation between create and set_parent). - Client-side self-reference guard (relays MUST reject cycles; fail early). - New set_parent_action.fbs (N29S) + wire codec; create_public_group.fbs gains optional parent (additive, SCHEMA_VERSION stays 1). NIP-11 detection: - RelayInfoDoc gains a generic feature_flags: BTreeMap<String, bool> carrier (D0-safe: no NIP noun in nmp-core; the "nip29.subgroups" key string is authored in nmp-nip11, not the substrate). - nmp-nip11 parse.rs fills feature_flags from the nip29.subgroups bool. Verified: cargo test -p nmp-nip29 (195 lib + integration) / nmp-nip11 (22) / nmp-core --lib substrate (121) all pass; cargo build --workspace clean; doctrine-lint D0 clean on nmp-core + nmp-nip29; all 4 codegen drift checks green (producer-consts, swift-flatc, KernelTypes, typed-decoders).
…etParent action, NIP-11 capability detection * feat(nmp-nip29): NIP-29 subgroups support (nips PR #2319) Add hierarchical subgroups per nostr-protocol/nips#2319. No new event kinds; the spec adds parent/child tag names on kind:39000 and a NIP-11 nip29.subgroups capability flag. NMP is a client/protocol crate, so this implements the read side (projections) + publish side (actions) + NIP-11 detection; relay-enforced rules (cycle rejection, bilateral consent, child-list auto-sync) remain the relay's job. Read side: - DiscoveredGroup + JoinedGroup gain parent: Option<String> and children: Vec<String> (ordered), folded from kind:39000 via a shared kinds::tags helper (one canonical parent/child extractor). - discovered_groups.fbs + joined_groups.fbs bump schema_version 1->2; Rust + Swift FlatBuffers bindings regenerated (flatc 25.12.19). - PROJECTION_CONTRACT versions bumped 1->2; producer consts regenerated. Publish side: - CreatePublicGroupInput gains optional parent (emits ["parent", id] on the kind:9002 metadata edit). - New SetParent action (nmp.nip29.set_parent) for adopt/detach, emitting a kind:9002 with only the parent tag (relay retains prior metadata). - Shared metadata_tags::metadata_edit_tags builder is the single canonical kind:9002 authoring path (no fragmentation between create and set_parent). - Client-side self-reference guard (relays MUST reject cycles; fail early). - New set_parent_action.fbs (N29S) + wire codec; create_public_group.fbs gains optional parent (additive, SCHEMA_VERSION stays 1). NIP-11 detection: - RelayInfoDoc gains a generic feature_flags: BTreeMap<String, bool> carrier (D0-safe: no NIP noun in nmp-core; the "nip29.subgroups" key string is authored in nmp-nip11, not the substrate). - nmp-nip11 parse.rs fills feature_flags from the nip29.subgroups bool. Verified: cargo test -p nmp-nip29 (195 lib + integration) / nmp-nip11 (22) / nmp-core --lib substrate (121) all pass; cargo build --workspace clean; doctrine-lint D0 clean on nmp-core + nmp-nip29; all 4 codegen drift checks green (producer-consts, swift-flatc, KernelTypes, typed-decoders). * fix: regenerate nip29 flatc bindings with pinned flatc 25.12.19 Regenerates discovered_groups_generated.rs and joined_groups_generated.rs using the canonical ci/regenerate-flatbuffers.sh script to resolve Rust flatc drift CI failure on PR #1899. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add SetParentInput to fail-closed test suite + trailing newlines Add SetParentInput to both assert_wrong_fid_rejected! and assert_bad_version! macros in tests_fail_closed.rs, and add the missing import. Fix missing trailing newlines on 4 files: set_parent_action.fbs, metadata_tags.rs, set_parent.rs, and subgroups.rs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(nmp-nip29): update ActorCommand import path after 3b-iv seal Commit 0bd6fdb removed the top-level `pub use actor::ActorCommand` re-export from `nmp_core`. Update the one remaining old-path import in `set_parent.rs` to use `nmp_core::actor::ActorCommand`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: pablof7z <marinadiazc@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
I was going to merge this and tried to fix the conflicts using the GitHub interface but I think I broke everything. Can you check, @Anderson-Juhasc? |
Adds a Subgroups section to NIP-29, letting groups be organized hierarchically. No new kinds: it reuses
kind:39000/kind:9002and adds two tag names,parentandchild.parenttag onkind:39000points at the parent'sdidentifier; noparent(or an empty one) means the group is a root. Clients build the tree locally fromkind:39000events.kind:9002(edit-metadata). The relay updates the group'skind:39000and keeps both sides in sync: it adds/removes the matchingchildtag on the parent (and on the previous parent when moving).kind:39000MUST list every child as an ordered["child", "<id>"]tag; relays reject metadata edits that drop existing children. Order is the tag position; admins can rearrange it.kind:9002setting["parent", "X"]is rejected unless the author is an admin of X as well (per X'skind:39001). Editing the child already requires admin on the child, so both sides must consent.parentpointing at a group that doesn't exist on the relay. When a parent is deleted (kind:9008), its remaining children automatically become roots.kind:9000/kind:9001to cascade members, but clients MUST NOT assume it.kind:39000from the relay serving the group is authoritative. Relays advertise subgroup support via NIP-11 (nip29: { subgroups: true }).parent; historical events under itshtag stay valid.