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
+147
View File
@@ -0,0 +1,147 @@
# 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:
```json
["-"]
```
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
```rust {file=coracle-lib/src/lib.rs}
pub mod protected;
```
```rust {file=coracle-lib/src/protected.rs}
//! 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::{Event, HashedEvent};
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.
```rust {file=coracle-lib/src/protected.rs}
/// 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 `["-"]`.
```rust {file=coracle-lib/src/tags.rs}
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. Both `HashedEvent` and `Event` carry a
`Tags` field, so the method facades delegate to the free function.
```rust {file=coracle-lib/src/protected.rs}
impl HashedEvent {
/// Whether this event carries the NIP-70 protected marker.
pub fn is_protected(&self) -> bool {
is_protected(&self.tags)
}
}
impl Event {
/// Whether this event carries the NIP-70 protected marker.
pub fn is_protected(&self) -> bool {
is_protected(&self.tags)
}
}
```
## Usage patterns
**Marking an event protected.** Attach the tag at template construction,
just like any other:
```rust
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:
```rust
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](26-relay-authentication.md) 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.
+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.
+230
View File
@@ -0,0 +1,230 @@
# Research: Protected Events
## Topic Summary
NIP-70 defines an optional, valueless `["-"]` tag — the "protected" marker.
Its presence asks relays to accept the event *only* when it is published
directly by its author, where "directly by its author" means the publishing
connection has authenticated via NIP-42 and the authenticated pubkey matches
the event's `pubkey`. A relay that receives a protected event over an
unauthenticated connection should issue a NIP-42 `AUTH` challenge and reject
the event for now; a relay that receives one from an authenticated connection
whose pubkey is *not* the author should reject it outright.
Like NIP-40 expiration, this is a **behavior tag**: orthogonal to the event's
kind, it requests cooperation from the network rather than altering what the
event means. It is a hint, not a guarantee — once an event is signed and held
by anyone, no tag can claw it back.
This chapter adds tag-layer support only, mirroring the Expiring Events
chapter: a `Tag::protected()` constructor, an `is_protected(&Tags)` predicate,
and `is_protected()` method facades on `HashedEvent` and `Event`. Relay-side
enforcement (the NIP-42 auth-then-compare logic) is deferred to the Relay
Authentication chapter (ch 26). The unusual property of this tag — that it
carries no value — is the only thing distinguishing the API from expiration:
there is nothing to parse, so there is no `get_*` function, just a boolean.
## Philosophy
`ref/building-nostr` addresses NIP-70 directly and frames it the way this
chapter should.
- **Behavior tags are cooperative requests.** The book classifies tags as
data, filter, or behavior tags, and names NIP-70 explicitly: *"the NIP 70
protected tag `['-']` has no values. The presence of the tag merely indicates
that relays should only accept such event if they're published directly by
their author."* Behavior tags "determine how implementations should handle an
event, independent of the specification which defines the event's kind." This
is exactly the framing already used for expiration — and a reader arriving
from chapter 9 will recognize it.
- **Signed data cannot be unpublished.** *"Once it's published, there's no way
to un-sign or un-publish the data. Anyone who has a copy can keep it."* The
protected tag does not make an event secret; it asks relays to limit who they
will *accept* it from going forward. The chapter must be honest that this is
intent-signaling, not enforcement.
- **Access control is the relay's job, via AUTH.** *"There is one additional
responsibility that relays can't really delegate: access control. Access
control on Nostr is implemented by the `AUTH` verb."* This is why NIP-70's
enforcement story belongs to the networking layer and NIP-42, not to the
stateless tag layer. The library's job here is only to read and write the
marker.
- **Censorship resistance vs. access control.** *"An architecture optimized for
censorship resistance is necessarily not optimized for access controls or
privacy."* Protected events ask relays to *limit* distribution, a deliberate
deviation from nostr's broadcast default — useful framing for why this tag
exists at all.
- **Routing implications.** The book notes that access-controlled content (it
cites NIP-29 groups) should *not* be routed by the usual outbox heuristic,
since that would "leak" content meant to be protected. A brief forward
reference: protected events interact with relay selection, but that is a later
concern.
## Reference Implementation Analysis
### applesauce
Supported, client-side only, in `applesauce-core`:
- `isProtectedEvent(event)``helpers/event.ts:160`:
`return event.tags.some((t) => t[0] === "-");`
- `setProtected(set = true)``operations/event.ts:91`: adds the `["-"]`
singleton tag, or removes it, via `setSingletonTag(["-"])` /
`removeSingletonTag("-")`.
- `protected(isProtected)``factories/event.ts:181`: fluent builder method on
`EventFactory`.
The `["-"]` tag is treated like any other singleton tag (`expiration`, `alt`),
with no special-casing. No relay enforcement, no NIP-42 coupling. Notably,
`isProtectedEvent` is never *called* elsewhere in the library except in tests —
it is a primitive offered to consumers.
### ndk
Supported as a getter/setter on `NDKEvent` (`core/src/events/index.ts:868`):
```typescript
set isProtected(val: boolean) { this.removeTag("-"); if (val) this.tags.push(["-"]); }
get isProtected(): boolean { return this.hasTag("-"); }
```
The one place protection is *consumed* is reposts (`repost.ts:24`): when
building a NIP-18 repost, a protected original is **not** JSON-embedded in the
repost content. No relay-side enforcement; NDK is a client library, so the
relay rule is out of scope by design.
### nostr-gadgets
**Not implemented.** No references to nip70/protected/`["-"]` anywhere. The
library is a high-level client utility layer (profile loading, lists, relay
hints, outbox) that delegates event construction to `@nostr/tools` and never
touches the protected tag.
### nostrlib
The most complete implementation, including relay enforcement. Dedicated
`nip70/nip70.go`:
- `IsProtected(event) bool` — iterates tags, returns true if any tag is exactly
`["-"]`.
- `HasEmbeddedProtected(event) bool` — for reposts (kind 6/16), detects an
embedded `["-"]` in the stringified content via substring search.
Relay enforcement lives in the khatru handler (`khatru/handlers.go:192`):
```go
if nip70.IsProtected(env.Event) {
authed, is := GetAuthed(ctx)
if !is {
RequestAuth(ctx) // NIP-42 challenge
// OK:false "auth-required: must be published by authenticated event author"
} else if authed != env.Event.PubKey {
// OK:false "blocked: must be published by event author"
}
} else if nip70.HasEmbeddedProtected(env.Event) {
// OK:false "blocked: can't repost nip70 protected"
}
```
There is also an opt-in policy `OnlyAllowNIP70ProtectedEvents`. No client-side
builder — callers append `["-"]` by hand. The auth-then-compare flow is the
canonical relay logic and is what our ch 26 will implement.
### nostr-tools
Minimal: the only handling is one inline check inside NIP-18 reposts
(`nip18.ts:41`):
```typescript
content: t.content === '' || reposted.tags?.find(tag => tag[0] === '-') ? '' : JSON.stringify(reposted),
```
If a reposted event is protected, the repost content is emptied. No standalone
detector, no builder, no relay rules — consistent with the library's low-level,
minimal philosophy. A single test covers the repost-emptying behavior.
### rust-nostr
First-class and the closest analog to our target design. The `-` tag is a named
enum variant rather than a generic string tag:
- `TagKind::Protected``"-"` (`event/tag/kind.rs`).
- `TagStandard::Protected`, parsed from `["-"]` (`event/tag/standard.rs`).
- `Tag::protected() -> Self` (`event/tag/mod.rs:469`).
- `Tag::is_protected(&self) -> bool` (`mod.rs:529`) —
`matches!(self.as_standardized(), Some(TagStandard::Protected))`.
- `Event::is_protected(&self) -> bool` (`event/mod.rs:263`) —
`self.tags.find_standardized(TagKind::Protected).is_some()`.
Relay enforcement in `nostr-relay-builder` mirrors nostrlib: if protected and
not NIP-42-authenticated, send AUTH; if authed pubkey ≠ event pubkey, reject
with "this event may only be published by its author". A `test_protected_event`
round-trips `{"tags":[["-"]]}` JSON. Clean, minimal, self-contained; no separate
NIP-70 module because the feature is small enough to live in the tag system.
### welshman
**Not implemented.** No nip70/protected/`["-"]` handling anywhere. Welshman has
generic tag utilities (`getTag`, `getTagValue`, specialized `p`/`e`/`a`/`t`
helpers) and a NIP-42 auth event builder (`makeRelayAuth`), but nothing wires
them to a protected-event rule. As the sibling library to this one, its absence
is a small data point that NIP-70 is often skipped — but our chapter adds it.
## Common Patterns
- **The tag is `["-"]`, valueless.** Every implementation that handles it agrees
presence alone is the signal; there is nothing to parse. This is the single
most important fact for our API: a boolean predicate, no `get_*`.
- **Detection is a one-liner.** Universally "does any tag's first element equal
`"-"`" — i.e., `tags.has("-")` in our existing `Tags` vocabulary.
- **Construction is trivial.** A constructor that returns `["-"]`. rust-nostr
gives it a named `Tag::protected()`; applesauce/ndk inline `["-"]`.
- **Method on the event is the ergonomic surface.** rust-nostr, ndk, and
applesauce all expose detection at the event level (`is_protected` /
`isProtected`), which is where callers actually hold the data.
- **Enforcement is relay-side and needs NIP-42.** nostrlib and rust-nostr
implement the identical flow: protected + unauthenticated → AUTH challenge +
reject; protected + authed-but-wrong-pubkey → reject. Client libraries
(applesauce, ndk, nostr-tools, welshman) do *not* enforce; they only mark.
- **The NIP-18 repost interaction recurs.** applesauce, ndk, nostr-tools, and
nostrlib all special-case reposts so a protected event isn't re-broadcast by
being embedded in someone else's repost content. This is a real, named
consequence of the tag worth a forward reference, but it belongs to a reposts
chapter, not here.
- **Two libraries skip it entirely** (nostr-gadgets, welshman), confirming the
feature is small and often deferred — which is why a focused, correct
tag-layer treatment is worth having.
## Considerations for Our Implementation
- **Mirror the expiration chapter's shape.** Free function on `&Tags`, a
`Tag::protected()` constructor in `tags.rs`, method facades on `HashedEvent`
and `Event`, a new `protected.rs` module, a hand-written integration test.
Consistency with ch 9 is a feature: the reader has just seen this exact shape.
- **No `get_*`, no parsing.** The tag has no value. The whole API is a boolean.
`is_protected(&Tags)` is `tags.has("-")` — our `Tags::has` already does the
work. The chapter should acknowledge that the free function is a thin wrapper
and justify it on consistency and discoverability grounds (a named NIP-70
function the reader can find), the same justification used for the expiration
constructor.
- **No `Timestamp`/value type concerns.** Unlike expiration there is no number,
so none of expiration's parsing/`<` discussion applies. The chapter is
shorter.
- **The constructor goes in `tags.rs`,** appended as another `impl Tag` block,
exactly like `Tag::expiration`. Discoverable next to `Tag::new`; no import
from `protected` just to build a tag.
- **Defer enforcement explicitly.** State the relay rule in prose — protected +
authed-author-only — and name where it will be implemented (Relay
Authentication, ch 26, which needs the NIP-42 machinery). Do not write
acceptance logic here; the tag layer is stateless and has no notion of a
connection or an authenticated pubkey.
- **Mention the repost consequence briefly,** as a forward reference, without
implementing it: a protected event should not be embedded in a NIP-18 repost.
- **No dependencies.** Pure std + existing crate types. No new `Cargo.toml`
entries, no workspace changes.
- **The `"-"` tag name is unusual** (a single dash, not a word). Worth one
sentence: it is intentionally terse and visually distinct, and it has no
values, which is why it does not look like other tags.
+3 -1
View File
@@ -15,7 +15,9 @@ fn fixed_secret() -> SecretKey {
fn make_signed_event(kind: u16, tags: Vec<Tag>) -> coracle_lib::events::Event { fn make_signed_event(kind: u16, tags: Vec<Tag>) -> coracle_lib::events::Event {
let sk = fixed_secret(); let sk = fixed_secret();
let pk = sk.public_key(); let pk = sk.public_key();
let hashed = EventContent::new("test", tags) let hashed = EventContent::new()
.content("test")
.tags(tags)
.kind(kind) .kind(kind)
.stamp(1_700_000_000) .stamp(1_700_000_000)
.own(pk) .own(pk)
+4 -2
View File
@@ -13,7 +13,9 @@ fn fixed_secret() -> SecretKey {
} }
fn build_hashed(content: &str, tags: Vec<Tag>) -> HashedEvent { fn build_hashed(content: &str, tags: Vec<Tag>) -> HashedEvent {
EventContent::new(content, tags) EventContent::new()
.content(content)
.tags(tags)
.kind(1) .kind(1)
.stamp(1_700_000_000) .stamp(1_700_000_000)
.own(fixed_secret().public_key()) .own(fixed_secret().public_key())
@@ -29,7 +31,7 @@ fn sign_fixture() -> Event {
#[test] #[test]
fn pipeline_progresses_through_types() { fn pipeline_progresses_through_types() {
let content = EventContent::new("hi", Tags::new()); let content = EventContent::new().content("hi").tags(Tags::new());
let template: EventTemplate = content.kind(1); let template: EventTemplate = content.kind(1);
let stamped: StampedEvent = template.stamp(1_700_000_000); let stamped: StampedEvent = template.stamp(1_700_000_000);
let owned: OwnedEvent = stamped.own(fixed_secret().public_key()); let owned: OwnedEvent = stamped.own(fixed_secret().public_key());
+3 -1
View File
@@ -15,7 +15,9 @@ fn fixed_secret() -> SecretKey {
fn make_event(kind: u16, timestamp: u64, tags: Vec<Tag>) -> Event { fn make_event(kind: u16, timestamp: u64, tags: Vec<Tag>) -> Event {
let sk = fixed_secret(); let sk = fixed_secret();
let hashed = EventContent::new("hello", tags) let hashed = EventContent::new()
.content("hello")
.tags(tags)
.kind(kind) .kind(kind)
.stamp(timestamp) .stamp(timestamp)
.own(sk.public_key()) .own(sk.public_key())
+3 -1
View File
@@ -12,7 +12,9 @@ fn fixed_secret() -> SecretKey {
} }
fn test_event() -> OwnedEvent { fn test_event() -> OwnedEvent {
EventContent::new("hello nostr", Tags::new()) EventContent::new()
.content("hello nostr")
.tags(Tags::new())
.kind(1) .kind(1)
.stamp(1_700_000_000) .stamp(1_700_000_000)
.own(fixed_secret().public_key()) .own(fixed_secret().public_key())
+125
View File
@@ -0,0 +1,125 @@
use coracle_lib::events::{EventContent, HashedEvent};
use coracle_lib::keys::SecretKey;
use coracle_lib::protected::is_protected;
use coracle_lib::tags::{Tag, Tags};
fn fixed_secret() -> SecretKey {
let bytes: [u8; 32] = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32,
];
SecretKey::from_hex(&hex::encode(bytes)).unwrap()
}
fn build_hashed(tags: Tags) -> HashedEvent {
EventContent::new()
.content("hi")
.tags(tags)
.kind(1)
.stamp(1_700_000_000)
.own(fixed_secret().public_key())
.hash()
}
// --- Tag::protected constructor ---
#[test]
fn tag_constructor_shape() {
let t = Tag::protected();
assert_eq!(t.name(), "-");
assert_eq!(t.len(), 1);
assert!(t.values().is_empty());
}
#[test]
fn tag_constructor_serializes_as_single_element() {
let t = Tag::protected();
assert_eq!(serde_json::to_string(&t).unwrap(), r#"["-"]"#);
}
// --- is_protected ---
#[test]
fn is_protected_false_when_tag_absent() {
let tags = Tags::new();
assert!(!is_protected(&tags));
}
#[test]
fn is_protected_false_with_unrelated_tags() {
let tags = Tags::from(vec![Tag::new("t", ["nostr"]), Tag::expiration(100)]);
assert!(!is_protected(&tags));
}
#[test]
fn is_protected_true_when_tag_present() {
let tags = Tags::from(vec![Tag::protected()]);
assert!(is_protected(&tags));
}
#[test]
fn is_protected_true_among_other_tags() {
let tags = Tags::from(vec![
Tag::new("t", ["nostr"]),
Tag::protected(),
Tag::expiration(100),
]);
assert!(is_protected(&tags));
}
#[test]
fn is_protected_detects_a_raw_dash_tag() {
// The marker is identified by name alone; a hand-built ["-"] is
// equivalent to Tag::protected().
let tags = Tags::from(vec![Tag::new("-", Vec::<String>::new())]);
assert!(is_protected(&tags));
}
// --- HashedEvent method facade ---
#[test]
fn hashed_event_reports_protected() {
let event = build_hashed(Tags::from(vec![Tag::protected()]));
assert!(event.is_protected());
}
#[test]
fn hashed_event_reports_unprotected() {
let event = build_hashed(Tags::new());
assert!(!event.is_protected());
}
// --- Event method facade ---
#[test]
fn signed_event_reports_protected() {
let sk = fixed_secret();
let hashed = build_hashed(Tags::from(vec![Tag::protected()]));
let sig = sk.sign(&hashed.id);
let signed = hashed.sign(sig);
assert!(signed.is_protected());
}
#[test]
fn protected_tag_survives_hashing_and_signing() {
// The tag is part of the canonical hash input, so it must round-trip
// through hash() and sign() intact and still verify.
let sk = fixed_secret();
let hashed = build_hashed(Tags::from(vec![Tag::protected()]));
let sig = sk.sign(&hashed.id);
let signed = hashed.sign(sig);
assert!(signed.verify_id());
assert!(signed.is_protected());
}
#[test]
fn unprotected_event_stays_unprotected_through_signing() {
let sk = fixed_secret();
let hashed = build_hashed(Tags::new());
let sig = sk.sign(&hashed.id);
let signed = hashed.sign(sig);
assert!(!signed.is_protected());
}