- Sashité for Developers
- Nostr
- NIPs
- 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: on the kinds its timing service covers, 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 — and the kinds it covers — 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
Covered kinds
The window makes a created_at trustworthy as a timing. Only the kinds whose created_at a consuming application reads as a timing need that, and a relay’s timing service covers exactly those: in the Sashité game suite, the founding kinds (3418, 3419, 3420, 3422 — their deadlines run from it), the Ply (3423) and the Conclusion (3425 — its cutoff). A profile (kind 0), a contact or relay list, a mute list, a Rule System (3417 — “its created_at is informational”), a Composition (3440 — “nothing depends on Composition ordering”) chronometer nothing; applied to them, the window would only refuse every event not signed in the very second the relay received it — every re-broadcast, every profile edited in another client, every relay-to-relay sync — for no timing gained.
A conforming timing relay therefore enforces the ingestion window below on its covered kinds and advertises them (§NIP-11 advertisement). On an uncovered kind it applies only its ordinary policy: it SHOULD keep a forward bound as hygiene (generous — minutes, as relays commonly do), and it MUST NOT be read as vouching for that kind’s created_at. A relay that advertises no covered-kinds list covers every kind — this profile’s original reading, preserved so an older advertisement keeps its meaning.
The ingestion window
A conforming timing relay enforces, on every kind its timing service covers:
- It MUST reject any event whose
created_atis in the past relative to the relay’s own clock, beyond a small past tolerance covering clock skew and one-way network delay. This is the anti-backdating rule: a signer cannot obtain a timing earlier than the moment the relay received the event. The past tolerance SHOULD be a few seconds at most; a relay admitting minutes of backdating is not a usable timing authority. - It SHOULD reject any event whose
created_atis in the future beyond a future tolerance — hygiene against forward-dating and stuck events, not an anti-cheat requirement (forward-dating is self-penalizing in most timing-consuming applications). The window MAY be asymmetric: tight against the past, where backdating lives; looser toward the future.
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.
Per-slot ceiling
Where a consuming application bounds the number of events one signer may have timed for one slot it owns (Canonical Timing §Bounding a signer’s candidates for one slot), a conforming timing relay enforces that ceiling at ingestion: it accepts at most M events of one signer for one slot — the first M it receives — and rejects any further one. The slot is whatever the consuming kind defines (in the game suite, kind 3423: the game_session-marked e tag, the signer’s pubkey and the step tag; M = 32, max_plies_per_slot). A rejection under the ceiling follows §Rejections (invalid: slot ceiling reached, the wording again not a wire contract); a rejected event was never stored, so it acquires no timing anywhere. The ceiling bounds every consumer’s work identically, at the source, which is why it is the relay’s and not the consumers’.
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), and the kinds the window covers as limitation.created_at_window_kinds — an array of kind numbers, ascending. An absent created_at_window_kinds means the window covers every kind (§Covered kinds); a present, empty array covers none and advertises no timing service at all. The bounds describe the window on the covered kinds only: whatever forward hygiene the relay applies to the other kinds is not advertised here. 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 of a kind the relay covers (§Covered kinds — listed in created_at_window_kinds, or every kind when the list is absent), read 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 event of an uncovered kind carries only its self-claimed created_at, wherever it was read. 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 of a covered kind requires created_at to be at or ahead of the relay’s clock (within the past tolerance), a publishing client SHOULD, for such a kind, 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
- The relay is the trust anchor. A malicious or misconfigured relay can accept backdated events and leaves no public artifact of having done so. Applications SHOULD treat relay-operator honesty as part of their trust model, prefer operators independent of other per-context authorities, and offer a signed-attestation mode where late verifiability matters.
- No late verifiability. Conformance vouches for timing at ingestion, toward the relay’s readership; it embeds no proof in the event. Consumers who cannot read from the conforming relay gain nothing from the profile.
- Mirrors. A mirror of a conforming relay preserves timing trust only if it replicates faithfully and its consumers trust that replication; the profile itself says nothing about mirrors.
- Rate limiting. A relay serving clock-timed applications SHOULD rate-limit acceptances per signer for flood-prone kinds, so a burst of candidates cannot inflate consumers’ work; the per-slot ceiling (§Per-slot ceiling) is the normative form of this for kinds that define slots, and the self-timed analogue of an attesting party’s ceiling.
Reference deployment
Sashité’s timing relay ships this profile with a past tolerance of 1 second, a future tolerance of 5 seconds on the covered kinds 3418, 3419, 3420, 3422, 3423, 3425 (a 15-minute forward bound and no past bound on every other kind it serves), and a per-slot ceiling of 32 Plies (kind 3423) per signer and step; 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
- NIP-01 —
created_at, theOKmessage and itsreason. - NIP-11 — the relay information document and its
limitationobject. - Event Timestamp Attestation — the signed-attestation alternative for portable, late-verifiable timing.
- Canonical Timing — how consuming applications turn relay-vouched
created_atinto canonical timing, pending state, and race resolution.
