Add protected chapter

This commit is contained in:
Jon Staab
2026-05-20 12:27:22 -07:00
parent f0d49c6fb8
commit 8ad0bc393b
8 changed files with 675 additions and 5 deletions
+160
View File
@@ -0,0 +1,160 @@
# 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
1. **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.
2. **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).
3. **The module declaration**`pub mod protected;` in `lib.rs` plus the module
header doc comment.
4. **The predicate**`is_protected(&Tags) -> bool`. A thin wrapper over
`Tags::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 no `Option`, just `bool`.
5. **Building the tag**`Tag::protected() -> Tag` returning `["-"]`. One line;
the narrative explains the unusual single-dash, valueless shape.
6. **Methods on event types**`is_protected(&self)` on both `HashedEvent` and
`Event`, delegating to the free function.
7. **Usage patterns** — building a protected event with the template builder;
checking on receipt. Brief.
8. **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`
```rust
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`
```rust
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. Mirrors `expiration.rs`.
- **`coracle-lib/src/lib.rs`** — add `pub mod protected;` (after `expiration`,
matching SUMMARY order: ch 10 follows ch 9).
- **`coracle-lib/src/tags.rs`** — append a `Tag::protected` constructor inside a
new `impl Tag` block (code blocks targeting the same file concatenate; another
`impl Tag` block at the end is valid Rust, exactly as `Tag::expiration` does).
- **`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 `protected` tag, 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 defended `Tag::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 `&Tags` plus 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 -> bool` is correct and matches every reference
(`isProtectedEvent`, `IsProtected`, `Event::is_protected`).
- **No `get_protected`.** Nothing to return but a bool, which `is_protected`
already gives. A `get_*` would imply a value that does not exist.
- **Free function on `&Tags` plus method facades.** Matches the
`get_expiration`/`get_pow` pattern exactly. The free function is reusable by
any caller holding a tag slice (e.g., a relay inspecting raw tags before
reconstructing an `Event`); the methods are the ergonomic surface.
- **Constructor in `tags.rs`, not `protected.rs`.** Lives with `Tag::new` and
`Tag::expiration`; discoverable where callers build tags; no import from
`protected` just 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 declining `set_expiration`.
- **Constructor uses an empty value iterator.** `Tag::new("-", iter::empty())`
produces `["-"]`. Verified against `Tag::new`'s signature (name plus an
`IntoIterator` of 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: keep `Tags` a generic container; the named function lives in
`protected.rs`.
- Whether to document the NIP-18 repost interaction in depth. Keep it to a single
forward-reference sentence; reposts are their own topic.