8.0 KiB
Plan: Protected Events
Topic Summary
NIP-70 defines an optional, valueless ["-"] tag — the "protected" marker.
Its presence asks relays to accept the event only when published directly by
its author: the publishing connection must be NIP-42-authenticated and the
authenticated pubkey must equal the event's pubkey. Relays receiving a
protected event over an unauthenticated connection should issue an AUTH
challenge and reject it for now; relays receiving one from an authenticated but
non-matching connection should reject it outright.
Like NIP-40 expiration, this is a behavior tag — orthogonal to kind, a
cooperative request rather than a guarantee. This chapter adds tag-layer support
only: a constructor, a boolean predicate, and method facades. Relay-side
enforcement is deferred to the Relay Authentication chapter (ch 26), which has
the NIP-42 machinery. The tag carries no value, so — unlike expiration — there
is nothing to parse and no get_* function.
Chapter Outline
- Framing — protected as a behavior tag, sibling to expiration; the
["-"]tag has no value; the cooperative, relay-enforced (not cryptographic) nature of the request. Pick up the thread chapter 9 left dangling. - What the relay rule is — state the NIP-42 auth-then-compare rule in prose so the reader understands what the tag asks for, and explain why none of that logic lives here (the tag layer is stateless; enforcement needs a connection and an authenticated pubkey — ch 26).
- The module declaration —
pub mod protected;inlib.rsplus the module header doc comment. - The predicate —
is_protected(&Tags) -> bool. A thin wrapper overTags::has("-"); justify it on consistency and discoverability (a named NIP-70 function), the same justification the expiration chapter used for its constructor. No value to parse means noOption, justbool. - Building the tag —
Tag::protected() -> Tagreturning["-"]. One line; the narrative explains the unusual single-dash, valueless shape. - Methods on event types —
is_protected(&self)on bothHashedEventandEvent, delegating to the free function. - Usage patterns — building a protected event with the template builder; checking on receipt. Brief.
- What's next — bridge forward: enforcement lands with relay authentication (ch 26); note the NIP-18 repost interaction (a protected event shouldn't be embedded in a repost) as a forward reference, not implemented here.
API Design
Module coracle-lib/src/protected.rs
use crate::events::{Event, HashedEvent};
use crate::tags::Tags;
/// True if the tag set carries the NIP-70 `["-"]` protected marker.
pub fn is_protected(tags: &Tags) -> bool;
impl HashedEvent {
pub fn is_protected(&self) -> bool;
}
impl Event {
pub fn is_protected(&self) -> bool;
}
is_protected(tags) is tags.has("-").
Tag constructor in coracle-lib/src/tags.rs
impl Tag {
/// Build a NIP-70 `["-"]` protected tag.
pub fn protected() -> Tag;
}
Implemented as Tag::new("-", std::iter::empty::<String>()) — a tag with the
name "-" and no values, serializing as ["-"].
Code Organization
coracle-lib/src/protected.rs— new module: the free function and the two method impls. Mirrorsexpiration.rs.coracle-lib/src/lib.rs— addpub mod protected;(afterexpiration, matching SUMMARY order: ch 10 follows ch 9).coracle-lib/src/tags.rs— append aTag::protectedconstructor inside a newimpl Tagblock (code blocks targeting the same file concatenate; anotherimpl Tagblock at the end is valid Rust, exactly asTag::expirationdoes).coracle-lib/tests/protected.rs— hand-written integration test, not tangled. Covers the constructor shape, the predicate (present / absent), the method facades on both event types, and tag survival through hash + sign.
Dependencies
None. Pure std and existing crate types. No new Cargo.toml entries, no new
workspace members.
Narrative Notes
- Lead by connecting to expiration. Chapter 9 ends with an explicit promise:
"The next chapter introduces another — NIP-70's
protectedtag, which tells relays not to accept an event from anyone but its author." Open by fulfilling that promise. The reader already has the behavior-tag mental model. - The valueless tag is the one novel thing. Spend a sentence on why
["-"]has no value and why that collapses the whole API to a boolean — there is no malformed-vs-absent distinction to make, no parse to fail. - Be honest about enforcement. This is the "hint, not guarantee" point from expiration, sharpened: the tag asks relays to require NIP-42 auth from the author. Without a cooperating relay, the tag does nothing. State the relay rule precisely (auth challenge, then pubkey comparison) so the reader knows what they're requesting, then say plainly that implementing it is ch 26's job.
- Justify the free function. It is
tags.has("-")and nothing more. Defend it the way ch 9 defendedTag::expiration: a named, discoverable NIP-70 entry point reads better at call sites and at the doc level than a bare string literal, and it keeps the protected-events surface symmetrical with the rest of the library (a free function on&Tagsplus method sugar on the events). - Keep it short. This is the shortest behavior-tag chapter — no parsing, no timestamp comparison, no clock. Resist padding. The interest is conceptual (what the tag asks of the network), not mechanical.
- Forward references, not detours. Mention enforcement (ch 26) and the repost interaction once each; do not explain them.
Design Decisions
- Boolean, not
Option. The tag has no value; presence is the entire signal.is_protected -> boolis correct and matches every reference (isProtectedEvent,IsProtected,Event::is_protected). - No
get_protected. Nothing to return but a bool, whichis_protectedalready gives. Aget_*would imply a value that does not exist. - Free function on
&Tagsplus method facades. Matches theget_expiration/get_powpattern exactly. The free function is reusable by any caller holding a tag slice (e.g., a relay inspecting raw tags before reconstructing anEvent); the methods are the ergonomic surface. - Constructor in
tags.rs, notprotected.rs. Lives withTag::newandTag::expiration; discoverable where callers build tags; no import fromprotectedjust to construct one. Identical placement reasoning to ch 9. - Enforcement deferred to ch 26. The acceptance rule needs an authenticated
connection and a pubkey to compare against — state, not a stateless tag query.
Putting a
should_accept-style helper here (the option the user declined) would strand half the logic away from where the connection lives. The chapter describes the rule and points forward. - No
set_protected(bool)builder helper. ndk/applesauce expose a toggle, but our builder surface is already chainable via.tags(Tags::new().add(...))/Tag::protected(). Adding a toggle would be ceremony, consistent with ch 9 decliningset_expiration. - Constructor uses an empty value iterator.
Tag::new("-", iter::empty())produces["-"]. Verified againstTag::new's signature (name plus anIntoIteratorof values); an empty iterator yields a one-element tag.
Open Questions
- Whether to show the relay-enforcement pseudocode inline. Decision: no — a prose statement of the rule is enough; code belongs in ch 26 where the connection and auth state exist. Showing non-tangled relay code here would invite the reader to think enforcement is part of this layer.
- Whether to add a
Tags-level convenience beyond the free function. Skip, per ch 9 reasoning: keepTagsa generic container; the named function lives inprotected.rs. - Whether to document the NIP-18 repost interaction in depth. Keep it to a single forward-reference sentence; reposts are their own topic.