Files
coracle-rust/book/plan/expiring-events.md
T
2026-05-20 11:07:12 -07:00

5.8 KiB

Plan: Expiring Events

Topic Summary

NIP-40 defines an optional expiration tag: ["expiration", "<unix_timestamp_seconds>"]. 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 declarationpub mod expiration; in lib.rs plus the module header.
  3. Reading the tagget_expiration(&Tags) -> Option<u64>. Explain missing-vs-malformed both collapsing to None.
  4. Testable predicateis_expired_at(&Tags, now: u64) -> bool. Explain the strict-< comparison.
  5. System-clock predicateis_expired(&Tags) -> bool, plus the private now_seconds() helper.
  6. ConstructorTag::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 typesget_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

pub fn get_expiration(tags: &Tags) -> Option<u64>;
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<u64>;
    pub fn is_expired_at(&self, now: u64) -> bool;
    pub fn is_expired(&self) -> bool;
}

impl Event {
    pub fn get_expiration(&self) -> Option<u64>;
    pub fn is_expired_at(&self, now: u64) -> bool;
    pub fn is_expired(&self) -> bool;
}

Tag constructor in coracle-lib/src/tags.rs

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.