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
- Framing — expiration as a behavior tag; why it lives on
Tagsrather than on kind; cooperative nature of the feature. - The module declaration —
pub mod expiration;inlib.rsplus the module header. - Reading the tag —
get_expiration(&Tags) -> Option<u64>. Explain missing-vs-malformed both collapsing toNone. - Testable predicate —
is_expired_at(&Tags, now: u64) -> bool. Explain the strict-<comparison. - System-clock predicate —
is_expired(&Tags) -> bool, plus the privatenow_seconds()helper. - Constructor —
Tag::expiration(ts: u64) -> Tag. Explain why we bother: a chapter's worth of call sites read better with a named constructor than withTag::new("expiration", [ts.to_string()]). - Methods on event types —
get_expiration,is_expired,is_expired_aton bothHashedEventandEvent. - Usage patterns — the expected call sites (a relay filter, a UI check, a builder attaching a tag), very briefly.
- 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— addpub mod expiration;.coracle-lib/src/tags.rs— add theTag::expirationconstructor inside an existingimpl Tagblock, tangled from the expiration chapter (code blocks targeting the same file concatenate; the new block adds anotherimpl Tagblock 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 equalsnowis still valid; it becomes expired the momentnowadvances. - The
get_expiration/is_expiredsplit mirrors the pattern fromget_powin 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. TheSystemTime::now()fallback to0on 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
Timestampnewtype.created_atisu64everywhere; this stays consistent. - No clock injection. The testable
is_expired_at(now)is enough; aClocktrait 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
&Tagsonly. The predicate doesn't need the event id or pubkey. Keeping it at theTagslevel makes it reusable for any caller holding a tag slice (e.g., a relay streaming raw JSON can check without reconstructing anEvent). - 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 toTag::new; avoids an import fromexpirationjust 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_nowonTagsdirectly. Skip it; the free function is already named clearly, and adding methods onTagsblurs the "tags are a generic container" line.