Sashité for Developers
  1. Sashité for Developers
  2. Nostr
  3. NIPs
  4. Self-Timed Timing Relay

NIP-XX

Self-Timed Timing Relay

draft optional

This NIP defines a relay profile: the strict created_at ingestion policy by which a relay becomes a usable timing authority for applications that treat an event’s own created_at as authoritative timing (“self-timed” applications), together with the NIP-11 advertisement consumers verify. It defines no event kind. The Canonical Timing NIP defines how consuming applications turn acceptance by a relay conforming to this profile into an authoritative timestamp.

Abstract

An application that orders events, runs clocks, or resolves races on the events’ own created_at needs that field to be trustworthy — yet created_at is a self-claim any signer can set freely. A relay conforming to this profile makes the claim trustworthy at ingestion: it refuses any event whose created_at lies outside a tight window around its own clock, so an accepted event’s created_at is, within the window’s tolerance, the moment the relay received it. The relay advertises the enforced window via NIP-11 so consumers can verify the policy before relying on it — and so publishing clients can classify a timing rejection by computation, with no message parsing.

Motivation

Signed third-party timestamps (e.g. an Event Timestamp Attestation) make timing portable but require a designated attesting party per context. Many applications instead want a zero-extra-party baseline: the relay the application already uses vouches for timing implicitly, by refusing to store what it did not receive now. This profile pins the two pieces such applications need to interoperate: the ingestion window, and the discoverable advertisement from which everything else — including a client’s retry decision — is derived.

Specification

The ingestion window

A conforming timing relay enforces, on every event kind its timing service covers:

Rejections

A rejection under either bound SHOULD use the standardized invalid: prefix in the OK message’s reason (NIP-01), and its human-readable remainder SHOULD name created_at for diagnosability (e.g. invalid: created_at too old). The reason text is not a wire contract: NIP-01 standardizes only the prefix, and deployed strict relays reject stale timestamps with arbitrary wording. A client MUST NOT key behavior on the reason text; it classifies a timing rejection by computation — it knows the created_at it sent, its own clock, and the relay’s advertised bounds (§NIP-11 advertisement), which side of the window it fell on follows (§Client publishing guidance).

A rejected event was never stored: it acquires no timing and carries no penalty beyond the retry.

NIP-11 advertisement

A conforming timing relay MUST advertise its enforced window in its NIP-11 information document, as limitation.created_at_lower_limit (the past tolerance) and limitation.created_at_upper_limit (the future tolerance). This profile requires both fields to be interpreted as deltas in seconds relative to the relay’s current time — how far into the past, respectively the future, an accepted created_at may lie. (NIP-11 itself does not fix delta-versus-absolute semantics in prose; the delta reading is the de-facto one, consistent with NIP-11’s own example values, and this profile makes it a requirement.) The advertised bounds MUST equal the enforced tolerances — derived from the same configuration, so the advertised and the enforced window cannot drift apart. Because the bounds are relative, the advertisement is a constant of the configuration: the document changes only when the configuration does.

Consumer rule

A consumer MUST rely on self-timed timing only for events it reads from a relay whose advertised past bound it has verified to be a few seconds at most (or from a mirror it trusts to be faithful to such a relay). An absent information document, absent limitation, or absent lower limit reads as not conforming. Events re-encountered outside a conforming relay carry only their self-claimed created_at; nothing distinguishes an honestly-timed event from a backdated one accepted by a lax relay. A consumer in that position MUST treat the event as pending per the Canonical Timing NIP §The pending state: no canonical timing is established until the event is retrieved from a conforming relay the consumer relies on.

Client publishing guidance

Because acceptance requires created_at to be at or ahead of the relay’s clock (within the past tolerance), a publishing client SHOULD set created_at to its best estimate of the relay’s current time plus a small forward buffer. On a rejection of a time-sensitive event, the client classifies by computation: if the created_at it sent is older than its clock estimate minus the advertised past tolerance, the rejection was stale-timing — it retries with a bumped value a bounded number of times and raises its per-relay clock-offset estimate; if the value was ahead of the estimate plus the advertised future tolerance, it lowers the estimate instead and never pushes created_at further forward. The reason text plays no part in this classification.

Security considerations

Reference deployment

Sashité’s timing relay ships this profile with a past tolerance of 1 second and a future tolerance of 5 seconds; its client treats a relay as conforming when the advertised past bound is at most 5 seconds, uses a 1-second forward buffer, a 2-second bump per retry (at most 3), and a per-relay clock offset clamped to ±60 seconds. These are deployment choices, not part of the profile; a client conforms through §Client publishing guidance alone.

References