Relay rejects spec-compliant NIP-17 DMs: gift-wrap (kind 1059) timestamps fail the freshness window
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
# Relay rejects spec-compliant NIP-17 DMs: gift-wrap (kind 1059) timestamps fail the freshness window
## Summary
NIP-59 instructs clients to randomize a gift wrap's `created_at` into the past — "The `created_at` timestamp SHOULD be tweaked to thwart time-analysis attacks" (canonically up to 2 days back; `nostr-tools` and other mainstream implementations do this by default). The Buzz relay applies its event-freshness window to the **outer kind-1059 wrap** and rejects it:
```
["OK", "", false, "invalid: event timestamp too far from server time"]
```
Practical effect: **no spec-following NIP-17 client can deliver a private DM through a Buzz relay.** The wraps produced by `nostr-tools`' `nip17.wrapEvent()` (its defaults backdate the wrap) are rejected 100% of the time.
## Reproduction
Against a relay at `63496cc1d` (observed 2026-08-01):
```js
import { wrapEvent } from 'nostr-tools/nip17';
const wrap = wrapEvent(senderSecretKey, { publicKey: recipientHex }, 'hello');
// wrap.created_at is randomized into the past per NIP-59
// send over authenticated WS:
ws.send(JSON.stringify(['EVENT', wrap]));
// → ["OK", id, false, "invalid: event timestamp too far from server time"]
```
Re-signing the outer wrap with `created_at = now` (fresh ephemeral key, same sealed payload) is accepted — confirming the freshness check on the wrap's timestamp is the only blocker.
## Where
`crates/buzz-relay/src/handlers/ingest.rs:1524` — the freshness rejection is applied uniformly, with no carve-out for `KIND_GIFT_WRAP` (`buzz-core/src/kind.rs:60`, already registered in the accepted-kinds tables).
## Suggested fix
Exempt kind 1059 from the `created_at` freshness window (or widen its window to NIP-59's two days plus skew). The wrap's timestamp is deliberately meaningless — ordering/retention decisions for gift wraps cannot rely on it anyway, and the inner seal/rumor timestamps are end-to-end encrypted. Rejecting backdated wraps only forces clients to defeat NIP-59's privacy mechanism (our current workaround: re-stamp the outer wrap to `now`, leaking exactly the timing metadata the spec hides).
## Side observation (probably intentional, mentioning for completeness)
The HTTP bridge rejects gift wraps outright with `invalid: kind 1059 is only accepted via WebSocket`. If that restriction is by design it may be worth a line in the bridge docs, since every other event kind we use round-trips fine over `POST /events`.
Contributor guide
Research direction
Start at crates/buzz-relay/src/handlers/ingest.rs:1524 and inspect the freshness check alongside the KIND_GIFT_WRAP definition in buzz-core/src/kind.rs:60. Verify the behavior with a kind-1059 WebSocket event using a deliberately backdated timestamp; done means spec-compliant gift wraps are accepted while freshness checks for other event kinds remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, rust
- Domain
- backend, networking
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100