Add protected chapter
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user