NIP-OA owner attestation silently discarded on closed relays when the agent is a direct relay member
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
**Describe the bug**
On a closed relay (`require_relay_membership = true`, `allow_nip_oa_auth = true`), submitting an event with a valid `x-auth-tag` NIP-OA owner attestation does **not** materialize `users.agent_owner_pubkey` if the signing agent is already a **direct relay member**. The attestation is accepted for transport and then silently dropped.
The behavior is inverted relative to open relays: an **open** relay extracts the owner from the auth tag regardless of membership, while a **closed** relay — the stricter deployment — only materializes the owner when the agent is *not* a direct member.
**Where** (verified against `main` @ `ac4fa13`, 2026-08-01):
`crates/buzz-relay/src/api/bridge.rs:812-818`:
```rust
Ok(owner) => owner.or_else(|| {
if !state.config.require_relay_membership {
super::relay_members::extract_nip_oa_owner(&pubkey_bytes, auth_tag)
} else {
None // closed relay + direct member => owner never extracted
}
}),
```
The interaction is with `check_relay_membership` (`crates/buzz-relay/src/api/mod.rs:61`): the direct-membership fast path returns `MembershipDecision::Member` (`mod.rs:78`), which maps to `Ok(None)` — no owner. Only the `ViaOwner` branch (`mod.rs:100`, mapped at `mod.rs:131-132`) carries the owner pubkey forward. So the owner is materialized exactly when membership was granted *via* the owner, and dropped when the agent satisfied membership directly — even though the attestation is identical and valid in both cases.
**Steps to reproduce**
1. Run a relay with `require_relay_membership = true` and `allow_nip_oa_auth = true`.
2. Generate an agent keypair; compute a NIP-OA auth tag with `buzz_sdk::nip_oa::compute_auth_tag`.
3. Add the agent as a **direct relay member** (`run.sh add-member --role member`).
4. As the agent, publish a kind:0 profile over `POST /events` with the `x-auth-tag` header set.
5. Query the DB:
```sql
SELECT encode(agent_owner_pubkey, 'hex') FROM users WHERE pubkey = decode('', 'hex');
```
**Expected behavior**
`agent_owner_pubkey` is set to the attested owner. A valid attestation should be honored regardless of which membership branch granted access.
**Actual behavior**
`agent_owner_pubkey` is `NULL`. The event is accepted (`{"accepted": true}`), so there is no signal that the attestation was ignored.
Removing the direct relay membership and republishing the **identical** event materializes the owner correctly — which is what makes the cause unambiguous.
**Impact**
- `channel_add_policy: owner_only` becomes unenforceable in the intended direction: with no owner on record, the policy's owner check has nothing to match. Provisioning an agent in the natural order (admit to relay → publish profile) yields an agent whose ownership never registers.
- Owner-scoped lookups (`buzz users get --name --owner `) return nothing.
- Silent: nothing in the response or logs indicates the tag was discarded.
**Suggested fix**
Extract and materialize the owner whenever a syntactically valid auth tag is present and verifies against the signing pubkey, independent of which membership branch granted access. Concretely: either have `check_relay_membership` return the verified owner alongside `Member`, or hoist the `extract_nip_oa_owner` call out of the `require_relay_membership` conditional in `bridge.rs`.
Happy to work on a PR for this if a maintainer confirms the intended direction.
**Version and platform**
- Buzz version: self-hosted relay, `ghcr.io/block/buzz:main` @ `sha256:13ddb4c12f4c0b8147cece717a57013a45fdb1610bd072d7e383ac79a070ddd9`; behavior re-verified in source on `main` @ `ac4fa13` (2026-08-01)
- OS: Ubuntu 24.04 (relay host), NIP-11 `supported_nips [1,2,10,11,16,17,23,25,29,33,38,42,50,56,43]`
**Logs / additional context**
Related issues (searched open and closed — no duplicate found): closest adjacent are the closed-relay bootstrap cluster #2969 / #3888 (membership timing/lockout, not attestation handling). Note the interaction: the bootstrap ordering those issues push operators toward (admit first, then publish) is exactly the ordering that triggers this bug.
Contributor guide
Research direction
Start in crates/buzz-relay/src/api/bridge.rs:812-818 and trace check_relay_membership in crates/buzz-relay/src/api/mod.rs, especially the Member and ViaOwner branches. Reproduce with a closed relay, a direct member, and a valid x-auth-tag, then verify that users.agent_owner_pubkey is populated; done means the same attestation is retained regardless of the membership branch.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- authentication, backend-api-design, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100