HTTP bridge NIP-98: byte-identical same-second requests are rejected as replays — the per-request-uniqueness convention is undocumented
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
**Describe the bug**
The HTTP bridge's NIP-98 replay guard keys on the **auth event id** (`crates/buzz-auth/src/nip98_replay.rs` — shared Redis seen-set, atomic set-if-absent, TTL ≥ 120s). That is the right design for actual replays, but it has a side effect for legitimate clients: a NIP-98 auth event is fully deterministic — kind `27235`, second-granularity `created_at`, `u` + `method` (+ optional `payload`) tags — so **two legitimate, distinct requests to the same URL within the same second serialize to the same event, produce the same id, and the second one gets `401 NIP-98: replay detected`**. For headless clients (bots, agents, integration daemons) that batch or burst requests, this is routine, not exotic.
Buzz's own bridge clients already handle this — but the obligation is documented nowhere:
- `crates/buzz-cli/src/client.rs:93-95` adds a random `nonce` tag to every per-request auth event, with the comment *"Nonce prevents replay rejection for rapid-fire requests with identical bodies."*
- `crates/buzz-acp/src/relay.rs` — same nonce approach.
- `NOSTR.md` — zero mentions of `nonce` or of any per-request-uniqueness requirement for NIP-98.
So a third-party integrator implementing NIP-98 exactly per the NIP (which does not require a nonce) gets intermittent 401s under load with no documented explanation.
For clarity, this report is about the **bridge routes only**. The Git HTTP transport is a different, deliberate regime: `git-credential-nostr` signs one repo-root-scoped credential that Git intentionally reuses across the requests of a session (`info/refs` GET → pack POST), and the transport therefore applies **no event-id dedup by design** — `crates/buzz-relay/src/api/git/transport.rs:193-198` documents that rejecting reused ids would break normal clone/push, with the ±60s window + URL scoping + HTTPS as the replay bound. That regime is coherent as-is and needs no change; it's mentioned only because the contrast is exactly what the docs should state: *per-request events with per-request uniqueness on the bridge; one intentionally reusable session credential on Git routes.*
**Steps to reproduce**
1. Run the relay with the HTTP bridge and Redis (standard dev setup).
2. From one keypair, issue two `POST /query` requests with the same body in the same wall-clock second, each with a freshly signed NIP-98 auth event containing only the standard `u`/`method`/`payload` tags (no nonce).
3. First request: 200. Second request: `401 NIP-98: replay detected` — the two auth events are byte-identical, hence id-identical.
We hit this with a headless integration daemon (Buzz rooms → external knowledge store: https://github.com/OriginTrail/buzz-dkg-integration) and reproduced it deterministically. One second-order finding worth documenting alongside the convention: the "obvious" workaround of monotonically bumping `created_at` to force uniqueness **drifts out of the verifier's ±60s freshness window under sustained bursts** — we shipped that first and it broke; a random `nonce` tag (exactly what `buzz-cli` does) is the correct scheme because it keeps timestamps honest.
**Expected behavior**
Document the client obligation: NIP-98 auth events sent to the bridge must be unique per request, and the recommended convention is an ignored random `nonce` tag (as `buzz-cli` and `buzz-acp` already do). A short note wherever the HTTP bridge gets documented — `NOSTR.md` currently doesn't cover it, so either a small bridge subsection there or the module docs — turns an intermittent mystery 401 into a documented contract, and can also state the bridge-vs-Git contrast above so nobody "fixes" the Git regime by mistake.
Happy to submit the docs PR (DCO-signed, Conventional Commits) if maintainers agree with the direction.
**Version and platform**
- Buzz version: observed live at `dd222a5`; code paths re-verified unchanged at current `main` (`485d03a`, 2026-07-29)
- OS: macOS 15 (relay built from source, host process; Postgres/Redis/MinIO in Docker)
**Logs / additional context**
```
HTTP/1.1 401 Unauthorized
NIP-98: replay detected
```
Relevant code, verified at `485d03a`:
- Replay guard design + id-keying: `crates/buzz-auth/src/nip98_replay.rs:1-31` (module doc: "Verify first, then mark", TTL floor 120s = 2×60s skew window)
- `buzz-cli` nonce convention + comment: `crates/buzz-cli/src/client.rs:90-96`
- Git transport's deliberate no-dedup regime: `crates/buzz-relay/src/api/git/transport.rs:144-157` (repo-root credential scope), `:193-198` ("NIP-98 event-ID dedup intentionally NOT implemented here… reuses one signed token across multiple requests")
- Duplicate search: no existing issue found for `nip-98 replay`, `replay detected`, `nonce` (searched 2026-07-29).
Contributor guide
Assessment
This issue has not been assessed yet.