Skip to content

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

Description

@Zigoljube

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 designcrates/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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions