block / block/buzz

HTTP bridge NIP-98: byte-identical same-second requests are rejected as replays — the per-request-uniqueness convention is undocumented

Open
#3,489 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.