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

138 lines
5.8 KiB
Markdown

# 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 declaration**`pub mod expiration;` in `lib.rs` plus
the module header.
3. **Reading the tag**`get_expiration(&Tags) -> Option<u64>`.
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<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`
```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.