192 lines
6.9 KiB
Markdown
192 lines
6.9 KiB
Markdown
# Expiring Events
|
|
|
|
Not every event is meant to last. A chat message, a transient status, a
|
|
time-limited invitation — these are events whose value decays to zero
|
|
at a known point in the future. NIP-40 gives them a way to say so: an
|
|
`expiration` tag carrying a Unix timestamp in seconds. After that
|
|
timestamp, the event is considered no longer valid.
|
|
|
|
The tag is a **behavior tag** — orthogonal to the event's kind. A kind
|
|
1 note with an expiration is still a note; the expiration is a request
|
|
that relays stop serving it and clients stop displaying it once the
|
|
time is up. The distinction matters. Nostr clients and relays already
|
|
agreed, per kind, on what the payload *means*. The behavior tag layer
|
|
lets authors add a separate instruction about what the network should
|
|
*do* with the event, without reopening the kind's semantics.
|
|
|
|
Expiration is a cooperative hint, not a cryptographic guarantee.
|
|
Anyone who has a copy of the event keeps that copy; the signature
|
|
remains valid forever. What the tag buys is a shared convention:
|
|
well-behaved relays will refuse to serve the event after the
|
|
timestamp, well-behaved clients will hide it, and storage layers that
|
|
care about disk space can sweep it. The library's job at this level is
|
|
only to parse the tag and answer "is this event expired?" — deciding
|
|
what to *do* about that answer belongs to code further up the stack.
|
|
|
|
## The module
|
|
|
|
```rust {file=coracle-lib/src/lib.rs}
|
|
pub mod expiration;
|
|
```
|
|
|
|
```rust {file=coracle-lib/src/expiration.rs}
|
|
//! NIP-40 expiring events: reading the `expiration` tag and checking
|
|
//! whether an event has expired.
|
|
//!
|
|
//! The free functions in this module operate on [`Tags`] so they can be
|
|
//! used wherever a tag slice is in hand. The [`EventExtensionExpiration`]
|
|
//! trait layers method-call sugar on top for any event type, reachable
|
|
//! through the crate prelude. The tag itself is built with
|
|
//! [`Tag::expiration`] from the tags module.
|
|
|
|
use crate::events::HasTags;
|
|
use crate::tags::Tags;
|
|
use crate::util::now;
|
|
```
|
|
|
|
## Reading the tag
|
|
|
|
The shape on the wire is `["expiration", "<unix_seconds>"]`. Parsing it
|
|
means finding the first tag with that name and interpreting its value
|
|
as a `u64`. Two ways to read this can go wrong — the tag may be absent,
|
|
or its value may not parse as a number — and both collapse to the same
|
|
answer: `None`. A malformed tag is indistinguishable from no tag at
|
|
all; neither tells us when the event should be considered expired, so
|
|
neither should produce a different downstream behavior.
|
|
|
|
```rust {file=coracle-lib/src/expiration.rs}
|
|
/// Return the expiration timestamp from a tag set, if present.
|
|
///
|
|
/// Reads the first `expiration` tag and parses its value as a Unix
|
|
/// timestamp in seconds. Returns `None` if the tag is missing or its
|
|
/// value does not parse as a `u64` — treating "malformed" the same as
|
|
/// "absent" avoids forcing every caller to distinguish a case they
|
|
/// cannot usefully act on.
|
|
pub fn get_expiration(tags: &Tags) -> Option<u64> {
|
|
tags.value("expiration").and_then(|v| v.parse().ok())
|
|
}
|
|
```
|
|
|
|
## A testable predicate
|
|
|
|
A predicate that reads the system clock is awkward to test. The
|
|
standard fix — take the clock as a parameter — costs nothing here
|
|
because the caller usually has a `now` value on hand anyway (a relay
|
|
checking a batch of incoming events picks a timestamp once and reuses
|
|
it, a UI rendering a timeline pins "now" at render time). The
|
|
two-function split keeps both audiences happy: callers who already
|
|
have a timestamp pass it in, and callers who just want to check
|
|
against wall-clock time call the shorter form that fills it in for
|
|
them.
|
|
|
|
The comparison is strict: `expiration < now`. An event whose
|
|
expiration equals the current time is still valid; it becomes expired
|
|
the moment `now` advances past it. "Expires at T" reads naturally as
|
|
"invalid after T," and that reading matches what every reference
|
|
implementation does.
|
|
|
|
```rust {file=coracle-lib/src/expiration.rs}
|
|
/// Test whether the tags declare an expiration that has already
|
|
/// passed at the given `now` timestamp.
|
|
///
|
|
/// Returns `false` if no expiration tag is present. The comparison is
|
|
/// strict: an event with expiration equal to `now` is *not* yet
|
|
/// expired.
|
|
pub fn is_expired_at(tags: &Tags, now: u64) -> bool {
|
|
matches!(get_expiration(tags), Some(t) if t < now)
|
|
}
|
|
```
|
|
|
|
## Against the system clock
|
|
|
|
The wall-clock form is a one-liner over `is_expired_at`, filling in the
|
|
timestamp from `crate::util::now`.
|
|
|
|
```rust {file=coracle-lib/src/expiration.rs}
|
|
/// Test whether the tags declare an expiration that has already
|
|
/// passed, relative to the system clock.
|
|
///
|
|
/// For batch checks or testable callers, prefer [`is_expired_at`] and
|
|
/// pass an explicit timestamp.
|
|
pub fn is_expired(tags: &Tags) -> bool {
|
|
is_expired_at(tags, now())
|
|
}
|
|
```
|
|
|
|
## Building the tag
|
|
|
|
We can add a small convenience method for building this tag:
|
|
|
|
```rust {file=coracle-lib/src/tags.rs}
|
|
impl Tag {
|
|
/// Build a NIP-40 `expiration` tag with the given Unix timestamp in seconds.
|
|
pub fn expiration(timestamp: u64) -> Tag {
|
|
Tag::new("expiration", [timestamp.to_string()])
|
|
}
|
|
}
|
|
```
|
|
|
|
## Methods on event types
|
|
|
|
Rather than repeat these queries on each event struct, expiration exposes
|
|
them through an extension trait bounded on [`HasTags`]. The method bodies
|
|
live once, as defaults, and a blanket impl makes them available on every
|
|
type that can hand back its tags — `HashedEvent`, `Event`, and anything
|
|
added later. With `coracle_lib::prelude::*` in scope, `event.is_expired()`
|
|
reads the same whether the event is hashed or signed.
|
|
|
|
```rust {file=coracle-lib/src/expiration.rs}
|
|
/// Expiration queries on any event that carries tags.
|
|
pub trait EventExtensionExpiration: HasTags {
|
|
/// Return the expiration timestamp of this event, if any.
|
|
fn get_expiration(&self) -> Option<u64> {
|
|
get_expiration(self.tags())
|
|
}
|
|
|
|
/// Whether this event has expired relative to the given timestamp.
|
|
fn is_expired_at(&self, now: u64) -> bool {
|
|
is_expired_at(self.tags(), now)
|
|
}
|
|
|
|
/// Whether this event has expired relative to the system clock.
|
|
fn is_expired(&self) -> bool {
|
|
is_expired(self.tags())
|
|
}
|
|
}
|
|
|
|
impl<T: HasTags> EventExtensionExpiration for T {}
|
|
```
|
|
|
|
```rust {file=coracle-lib/src/prelude.rs}
|
|
pub use crate::expiration::EventExtensionExpiration;
|
|
```
|
|
|
|
## Usage patterns
|
|
|
|
**Building an expiring event.** Attach the tag at template
|
|
construction:
|
|
|
|
```rust
|
|
let event = EventContent::new()
|
|
.content("this link is good for an hour")
|
|
.tags(Tags::new().add_tag(Tag::expiration(now + 3600)))
|
|
.kind(1)
|
|
.stamp(now)
|
|
.own(pubkey);
|
|
```
|
|
|
|
**Filtering out expired events.** Relays and clients both might need to
|
|
remove expired events from a collection. Using `is_expired_at` avoids the
|
|
need to re-generate the current timestamp repeatedly:
|
|
|
|
```rust
|
|
let now = util::now();
|
|
incoming.retain(|e| !e.is_expired_at(now));
|
|
```
|
|
|
|
## What's next
|
|
|
|
Expiration is one kind of behavior tag. The next chapter introduces
|
|
another — NIP-70's `protected` tag, which tells relays not to accept
|
|
an event from anyone but its author.
|