mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Repair stale large channel roster snapshots (#6251)
## Summary - detect relay-authored NIP-29 kind 39002 roster snapshots truncated by the former 1,000-member query cap and repair stale large rosters during relay startup - serialize canonical roster capture and replacement with membership writes, preserving tenant, channel, signer, pubkey, and role boundaries through mixed-version deployments - install migration 0032's fail-closed roster fence on the partitioned events table and verify its catalog shape plus behavior before opening relay listeners ## Rollout Migration 0032 is a hard schema-before-code compatibility boundary. Apply migrations before rolling this relay version. Startup refuses to open listeners when the parent/partition triggers are missing, disabled, mis-shaped, or behaviorally inert. For large installations, prefer `buzz-admin migrate` and monitor lock acquisition as documented in the chart README. ## Validation Exact head: `bcbba271f54bc0046a6683007e5a2b70403a11d5` - rebased onto `569308c23c9c2bf620dd3a9a5e4baecbcfa22e16`; the nine-file feature patch is byte-identical to pre-rebase head `be8ea0084f4d4c78c7c2550baad4399e4df8ce73` - pre-push hook passed at exact head: branch-skew, file-size, full Rust unit suite, Desktop Tauri clippy, and Desktop Tauri tests - `cargo fmt --all -- --check` - `cargo test -p buzz-relay group_members_snapshot_keeps_members_past_one_thousand -- --nocapture` - focused CI-mode Playwright regression: `selected relay agents revoked after the invite prompt cause no side effects` passed at exact head - prior exact-patch validation: `large_roster_reconciliation_candidates_respect_snapshot_count_and_signer`, mixed-writer locking/rollback, migration admission, partition trigger coverage, and desired-schema parity regressions ## Review Independent DB/relay review found no blocking issues in the exact feature patch. The concurrency fence holds the established replacement and membership locks on one transaction/connection through replacement; failures roll back both soft-delete and insert. Reconciliation remains tenant/channel/signer scoped and validates exact normalized pubkey-plus-role membership. The prior red Desktop shard was unrelated to this backend-only diff: its mocked mention test exercises no relay, database, or migration path. It reproduced as a timing flake on the old head, passed on retry/base, and now passes locally after rebasing onto current main. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -990,6 +990,85 @@ AFTER INSERT ON events
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
FOR EACH ROW EXECUTE FUNCTION refresh_channel_ttl_after_event_insert();
|
||||
|
||||
-- Channel roster snapshot fence (keep in sync with migrations/0032).
|
||||
-- Prevent mixed-version relay pods from publishing a stale NIP-29 member
|
||||
-- snapshot after a newer canonical roster has been committed.
|
||||
--
|
||||
-- Old binaries already serialize kind 39002 replacement on the replacement
|
||||
-- advisory key. This trigger adds the channel-membership key at INSERT time,
|
||||
-- after that canonical key, and validates every p tag against the current
|
||||
-- active membership set and roles. New binaries take both keys in the same
|
||||
-- order before capture and replacement. Thus old and new writers remain
|
||||
-- compatible during a rolling deploy.
|
||||
CREATE OR REPLACE FUNCTION guard_channel_roster_snapshot()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
canonical_members TEXT[];
|
||||
snapshot_members TEXT[];
|
||||
BEGIN
|
||||
IF NEW.kind <> 39002 OR NEW.channel_id IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended(
|
||||
'buzz_channel_membership:' || NEW.community_id::text || ':' || NEW.channel_id::text,
|
||||
0
|
||||
));
|
||||
|
||||
SELECT COALESCE(
|
||||
array_agg(encode(cm.pubkey, 'hex') || ':' || cm.role::text ORDER BY cm.pubkey),
|
||||
ARRAY[]::TEXT[]
|
||||
)
|
||||
INTO canonical_members
|
||||
FROM channel_members cm
|
||||
WHERE cm.community_id = NEW.community_id
|
||||
AND cm.channel_id = NEW.channel_id
|
||||
AND cm.removed_at IS NULL;
|
||||
|
||||
-- A roster is canonical only when every p tag uses the emitted four-field
|
||||
-- shape, contains a 32-byte hex pubkey and valid authoritative role, has no
|
||||
-- duplicate members, and exactly matches the active membership rows.
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements(NEW.tags) AS roster_tag(tag_json)
|
||||
WHERE roster_tag.tag_json->>0 = 'p'
|
||||
AND (
|
||||
jsonb_array_length(roster_tag.tag_json) <> 4
|
||||
OR COALESCE(roster_tag.tag_json->>1, '') !~ '^[0-9a-fA-F]{64}$'
|
||||
OR roster_tag.tag_json->>2 <> ''
|
||||
OR COALESCE(roster_tag.tag_json->>3, '') NOT IN ('owner', 'admin', 'bot', 'member', 'guest')
|
||||
)
|
||||
) THEN
|
||||
RAISE EXCEPTION 'kind 39002 roster contains an invalid p tag'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
SELECT COALESCE(
|
||||
array_agg(
|
||||
lower((roster_tag.tag_json->>1)) || ':' || (roster_tag.tag_json->>3)
|
||||
ORDER BY decode((roster_tag.tag_json->>1), 'hex')
|
||||
),
|
||||
ARRAY[]::TEXT[]
|
||||
)
|
||||
INTO snapshot_members
|
||||
FROM jsonb_array_elements(NEW.tags) AS roster_tag(tag_json)
|
||||
WHERE roster_tag.tag_json->>0 = 'p';
|
||||
|
||||
IF snapshot_members IS DISTINCT FROM canonical_members THEN
|
||||
RAISE EXCEPTION 'kind 39002 roster does not match canonical channel membership'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_events_guard_channel_roster_snapshot ON events;
|
||||
CREATE TRIGGER trg_events_guard_channel_roster_snapshot
|
||||
BEFORE INSERT ON events
|
||||
FOR EACH ROW EXECUTE FUNCTION guard_channel_roster_snapshot();
|
||||
|
||||
|
||||
-- Replica-fence floor guard (keep in sync with migrations/0021). A deferred
|
||||
-- constraint trigger re-checks, inside COMMIT processing, that channel-bearing
|
||||
-- event rows are no older than `buzz.created_at_floor` seconds before commit
|
||||
|
||||
Reference in New Issue
Block a user