feat(relay): add operator community provisioning - #1657
Conversation
4fecafd to
5301f3d
Compare
Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
5301f3d to
de5fd17
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de5fd17d24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "POST", | ||
| "/operator/communities", | ||
| None, | ||
| Some(&body), |
There was a problem hiding this comment.
Require a signed payload for provisioning bodies
When an operator client signs this POST without a NIP-98 payload tag, verify_bridge_auth still accepts the auth event and only binds the URL/method, leaving the JSON body (host and initial_owner_pubkey) unsigned. In that case, anyone who obtains a fresh Authorization: Nostr ... header can race the legitimate request with a different body and create or rotate a community before the replay guard records the event; the operator provisioning endpoint should reject body-bearing requests unless the auth event includes a matching payload hash.
Useful? React with 👍 / 👎.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 found three issues worth fixing before merge:
-
high: require signed payloads for provisioning bodies
POST /operator/communitiesaccepts NIP-98 auth without requiring apayloadtag. In that case the auth event binds only URL + method, leavinghostandinitial_owner_pubkeyunsigned. Anyone who gets a freshAuthorization: Nostr ...header can race it with a different body and create or rotate a community before the replay guard records the event. Please reject body-bearing provisioning requests unless the NIP-98 event includes a matching payload hash. -
medium: validate community hosts as real authorities before storing
validate_hostchecks normalization and a few forbidden characters, but still accepts syntactically invalid authorities like:, malformed bracketed IPv6,example..com, labels with underscores, or overlong labels. Those can be inserted intocommunities.hosteven though they may never correspond to a valid HTTP Host authority. Please parse/validate ashost[:port]and store only canonical normalized authorities. -
medium: avoid reporting
createdafter a racing upsert conflictProvisioning computes
existedwith a separate lookup beforeensure_configured_community. Two concurrent operator requests can both observeexisted = false; one inserts, the other hits the upsert conflict but still returnsstatus: "created". Please make the DB helper return whether the insert actually won, or do the check+insert in one transaction/CTE.
Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
…rose Stacked on block#1657 (mini/community-provisioning). The operator plane (POST /operator/communities) is the first runtime surface that can mint tenancy, so it gets the same mechanized treatment as the rest of the multi-tenant model: Tamarin (docs/spec/MultiTenantAuth.spthy, new S9 section): - Rules: Register_Operator / Compromise_Operator_Key, operator-signed provision + rotate requests (NIP-98 preimage includes the payload hash h(<host, owner>) — payload binding mandatory), relay provision + converge/rotate gated on the !OperatorKey allowlist, and the UniqueHostBinding restriction importing the append-only host map. - Lemmas: provisioning_requires_operator_authorization (allowlist gate, no compromise disjunct — compromise doesn't extend the allowlist), provision_accepts_only_operator_signed_payload (payload binding), rotation_confined_to_host_community (S5/S6-style single witness), plus three exists-trace probes incl. the create-then-converge lifecycle. - Full run: all 38 lemmas verified green (Tamarin 1.12.0 / Maude 3.5.1, ~141 s). Three commented mutations confirmed red: MUTATION_Provision_Unsigned_Body (the optional-payload-tag gap from the block#1657 review; 8-step trace), MUTATION_Provision_Any_Client (8 steps), MUTATION_Rotate_Ignore_Host (12 steps). TLA+ (docs/spec/MultiTenantRelay.tla, comment-only): stated assumption P-HOST-APPEND — HostCommunity stays a constant-per-segment function because the host map is append-only (ensure_configured_community's ON CONFLICT upsert never re-points a bound host), so runtime provisioning needs no TLC remodeling. SANY-checked. Prose (docs/multi-tenant-relay.md): S9 section under Authorization soundness, verification-status update (32 -> 38 lemmas, current run figures), P-HOST-APPEND conformance row (incl. the require-payload-tag and signer-checked-allowlist obligations), Mechanized Verification bullet, machine-check-hygiene classification for the new lemmas, and a stale spthy line-reference fix (:403/:413 -> :427/:437). Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Why
Enable kgoose or another deployment operator to provision and inspect Buzz communities without direct Postgres access, while preserving the relay's host-derived tenant boundary and keeping the Nostr event data plane unchanged.
What
POST /operator/communitiesas a standalone NIP-98-signed operator endpointGET /operator/communities?owner_pubkey=<hex>for kgoose's list-owned-communities readGET /operator/communities/availability?host=<host>so the relay is the source of truth for host normalization and availability checksRELAY_OPERATOR_PUBKEYS, fail closed by default, and hard-error on malformed configured pubkeyscommunitiesensure/create, optional owner bootstrap, and availability host normalizationinitial_owner_pubkeyon an existing community rotates the owner via the same bootstrap path asRELAY_OWNER_PUBKEY9037, no relay-membership bypass in/events, no ingest routing carve-out, no storage/fan-outRisk Assessment
Medium — this adds a deployment-root authority surface. The blast radius is bounded by NIP-98 request signing, replay protection, explicit
RELAY_OPERATOR_PUBKEYSallowlist, strict host/pubkey validation, and a no-op default config. Keeping this as a separate operator endpoint avoids special cases in the tenant-scoped event ingest path.References
cargo fmtcargo fmt --checkcargo check -p buzz-relaycargo test -p buzz-core no_duplicate_kind_values --libcargo test -p buzz-relay community_provisioning --libcargo test -p buzz-relay relay_operator_pubkeys --libcargo test -p buzz-db list_communities_owned_by_returns_only_owner_rows --lib(ignored by default as Postgres-dependent)sadscan,rust-fmt,web-fix,desktop-tauri-fmt,desktop-fix,mobile-fixcheck-push-org,desktop-test,mobile-test,desktop-tauri-test,rust-testsGenerated with Codex