161 lines
8.0 KiB
Markdown
161 lines
8.0 KiB
Markdown
# 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.
|