# Plan: Expiring Events ## Topic Summary NIP-40 defines an optional `expiration` tag: `["expiration", ""]`. It marks an event as no longer valid after the given time. Relays SHOULD stop serving expired events and MAY drop them; clients SHOULD hide them. Expiration is a cooperative hint, not a cryptographic guarantee. This chapter adds tag-layer support: a parser, a predicate, a constructor, and the usual method facades on `HashedEvent` and `Event`. Enforcement (storage sweeps, UI hiding) is out of scope. ## Chapter Outline 1. **Framing** — expiration as a behavior tag; why it lives on `Tags` rather than on kind; cooperative nature of the feature. 2. **The module declaration** — `pub mod expiration;` in `lib.rs` plus the module header. 3. **Reading the tag** — `get_expiration(&Tags) -> Option`. Explain missing-vs-malformed both collapsing to `None`. 4. **Testable predicate** — `is_expired_at(&Tags, now: u64) -> bool`. Explain the strict-`<` comparison. 5. **System-clock predicate** — `is_expired(&Tags) -> bool`, plus the private `now_seconds()` helper. 6. **Constructor** — `Tag::expiration(ts: u64) -> Tag`. Explain why we bother: a chapter's worth of call sites read better with a named constructor than with `Tag::new("expiration", [ts.to_string()])`. 7. **Methods on event types** — `get_expiration`, `is_expired`, `is_expired_at` on both `HashedEvent` and `Event`. 8. **Usage patterns** — the expected call sites (a relay filter, a UI check, a builder attaching a tag), very briefly. 9. **What's next** — bridge to protected events (chapter 10). ## API Design ### Module `coracle-lib/src/expiration.rs` ```rust pub fn get_expiration(tags: &Tags) -> Option; pub fn is_expired_at(tags: &Tags, now: u64) -> bool; pub fn is_expired(tags: &Tags) -> bool; impl HashedEvent { pub fn get_expiration(&self) -> Option; pub fn is_expired_at(&self, now: u64) -> bool; pub fn is_expired(&self) -> bool; } impl Event { pub fn get_expiration(&self) -> Option; pub fn is_expired_at(&self, now: u64) -> bool; pub fn is_expired(&self) -> bool; } ``` ### Tag constructor in `coracle-lib/src/tags.rs` ```rust impl Tag { pub fn expiration(timestamp: u64) -> Tag; } ``` Rationale for placement: the constructor lives with `Tag` (not in `expiration.rs`) because it belongs to the tag-construction surface, which callers already import when they build tags. This matches how the nonce tag is handled — built inline at its mining site — but gives `expiration` a nicer call site because building this tag is common. ## Code Organization - **`coracle-lib/src/expiration.rs`** — new module for the free functions and the method impls. - **`coracle-lib/src/lib.rs`** — add `pub mod expiration;`. - **`coracle-lib/src/tags.rs`** — add the `Tag::expiration` constructor inside an existing `impl Tag` block, tangled from the expiration chapter (code blocks targeting the same file concatenate; the new block adds another `impl Tag` block appended at the end, which is valid Rust). - **`coracle-lib/tests/expiration.rs`** — integration test (hand- written, not tangled). ## Dependencies None. `std::time::{SystemTime, UNIX_EPOCH}` is already in std. No new crate dependencies, no new workspace entries. ## Narrative Notes - Lead with the behavior-tag framing. A reader who has just finished proof of work will recognize the pattern: a tag that *requests* downstream cooperation. Expiration is the cleaner example. - Make the "hint, not guarantee" point explicitly. The chapter is not asserting that expired events are gone — it's giving callers the primitive they need to act on the hint. - Strict-`<` deserves a sentence. An event whose expiration equals `now` is still valid; it becomes expired the moment `now` advances. - The `get_expiration` / `is_expired` split mirrors the pattern from `get_pow` in the previous chapter: a free function that takes tags, plus method sugar on the event types. Call this out briefly — the consistency is the point. - Don't over-design `is_expired`. The `SystemTime::now()` fallback to `0` on a clock error means an unreachable case maps to "never expired," which is the safer default than "always expired". - Keep the constructor section short. It's one line of code; the narrative justifies adding it at all. ## Design Decisions - **No `Timestamp` newtype.** `created_at` is `u64` everywhere; this stays consistent. - **No clock injection.** The testable `is_expired_at(now)` is enough; a `Clock` trait would be ceremony in a library this size. - **Strict `<` not `<=`.** Matches rust-nostr, welshman, applesauce. "Expires at T" reads as "invalid after T". - **Free functions take `&Tags` only.** The predicate doesn't need the event id or pubkey. Keeping it at the `Tags` level makes it reusable for any caller holding a tag slice (e.g., a relay streaming raw JSON can check without reconstructing an `Event`). - **No background sweeper, no storage integration.** That belongs in a later storage chapter, if at all. This chapter is the parse layer. - **Tag constructor in `tags.rs`.** Discoverable next to `Tag::new`; avoids an import from `expiration` just to build a tag. - **No `set_expiration(ts)` helper on event builders.** Callers can write `.tags(tags.add_tag(Tag::expiration(ts)))` or the equivalent; the builder surface is already chainable enough. ## Open Questions - Whether to document the interaction with NIP-09 (deletion requests) in this chapter. Keep it out — deletion is its own chapter's problem, and entangling them here dilutes the focus. - Whether to add a convenience `is_expired_now` on `Tags` directly. Skip it; the free function is already named clearly, and adding methods on `Tags` blurs the "tags are a generic container" line.