Files
coracle-rust/book/10-protected-events.md
T
2026-05-20 14:27:26 -07:00

5.2 KiB

Protected Events

The previous chapter introduced behavior tags through expiration: a tag that requests something of the network without touching what the event itself means. NIP-70 adds another, aimed at a different concern. Where expiration asks relays to stop serving an event after a time, NIP-70's protected tag asks them to be careful about who they accept it from in the first place. Its shape on the wire is a single, valueless tag:

["-"]

That is the whole thing — a tag whose name is a lone dash and which carries no value. Its mere presence is the signal. An event marked this way should be accepted by a relay only when it is published directly by its author: the connection delivering it must have authenticated over NIP-42, and the authenticated key must match the event's pubkey.

What the marker asks for

A relay that honors NIP-70 runs a small decision when a protected event arrives. If the connection has not authenticated, the relay issues a NIP-42 AUTH challenge and rejects the event for now, inviting the client to prove who it is and try again. If the connection has authenticated but the authenticated key is not the event's author, the relay rejects the event outright — someone other than the author is trying to push it. Only an authenticated author gets through.

As with expiration, the marker is a cooperative request, not a guarantee. A relay is free to ignore it; a relay that has already stored the event keeps it; the signature stays valid regardless. The tag asks well-behaved relays to gate acceptance on authorship.

The module

pub mod protected;
//! NIP-70 protected events: the valueless `["-"]` marker that asks
//! relays to accept an event only from its authenticated author.
//!
//! This module is the stateless half of NIP-70 — building the tag and
//! detecting it. The relay-side enforcement it implies (a NIP-42 `AUTH`
//! challenge followed by a pubkey comparison) needs a live, authenticated
//! connection and is handled in the relay layer.

use crate::events::HasTags;
use crate::tags::Tags;

Detecting the marker

Because the tag has no value, detection collapses to a single question: is a tag named "-" present? There is nothing to parse, so there is no Option and no malformed-versus-absent distinction to draw — the answer is a plain bool. Tags::has already answers exactly this.

/// Whether the tag set carries the NIP-70 `["-"]` protected marker.
///
/// A protected event asks relays to accept it only from its
/// authenticated author. This function reports the marker's presence;
/// acting on it is the relay's job.
pub fn is_protected(tags: &Tags) -> bool {
    tags.has("-")
}

Building the tag

The constructor produces a tag with the name "-" and no values. We build it through Tag::new like every other tag, passing an empty value iterator so the result is the one-element ["-"].

impl Tag {
    /// Build a NIP-70 `["-"]` protected tag.
    ///
    /// The tag has a name (`"-"`) and no values; its presence asks
    /// relays to accept the event only from its authenticated author.
    pub fn protected() -> Tag {
        Tag::new("-", std::iter::empty::<String>())
    }
}

Methods on event types

A caller holding an event wants to ask whether it is protected without reaching for its tags directly. Following the pattern from expiration, the query is an extension trait bounded on [HasTags], so one default method serves every event type. With coracle_lib::prelude::* in scope, event.is_protected() works on both HashedEvent and Event.

/// Protected-event queries on any event that carries tags.
pub trait EventExtensionProtected: HasTags {
    /// Whether this event carries the NIP-70 protected marker.
    fn is_protected(&self) -> bool {
        is_protected(self.tags())
    }
}

impl<T: HasTags> EventExtensionProtected for T {}
pub use crate::protected::EventExtensionProtected;

Usage patterns

Marking an event protected. Attach the tag at template construction, just like any other:

let event = EventContent::new()
    .content("members only")
    .tags(vec![Tag::protected()])
    .kind(1)
    .stamp(now)
    .own(pubkey);

Checking on receipt. A relay or client deciding how to handle an incoming event asks the event directly:

if event.is_protected() {
    // only accept from an authenticated author
}

What's next

Marking an event protected is half of NIP-70; honoring the mark is the other half, and it lives where connections and authentication do. The Relay Authentication chapter implements the AUTH exchange and the author check that turn this tag into a rule.

One consequence is worth flagging now: a protected event should not be re-broadcast by being embedded inside someone else's repost, since that would route it past the very acceptance check it asked for. Reposts are their own topic; the protected marker is simply something that logic will need to consult.