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

Open Beginner friendly
#3,489 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Documentation
Clarity
Clearly specified
Activity status
Quiet
Tech stack
redis, rust

Research direction

Start with NOSTR.md and compare its HTTP bridge coverage with crates/buzz-auth/src/nip98_replay.rs, crates/buzz-cli/src/client.rs, and crates/buzz-relay/src/api/git/transport.rs. Document per-request uniqueness for bridge NIP-98 events, recommend a random nonce tag, explain why changing created_at is unsuitable, and contrast this with reusable Git credentials.

Written by the indexing model from the issue text.

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 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).
Dominant language
Rust
Stars
33.7k
Forks
4.4k
Avg merge
1d 21h
Merged PRs (30d)
239

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from block/buzz

All issues in block/buzz

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.