{"record":{"id":"54ebe9c94bc6c27c","repo":"block/buzz","slug":"failed-to-build-member-tag-e","errorCode":null,"errorMessage":"failed to build member tag: {e}","messagePattern":"failed to build member tag: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/buzz-admin/src/main.rs","lineNumber":366,"sourceCode":"        )\n        .await?\n        .map(|e| e.event.created_at.as_secs());\n\n    // custom_created_at = max(now, existing + 1s) — defeats same-second domination.\n    let ts = match newest_ts {\n        Some(existing) => (existing + 1).max(now),\n        None => now,\n    };\n\n    let members = db.list_relay_members(tenant.community()).await?;\n\n    let mut tags: Vec<Tag> = Vec::with_capacity(members.len() + 1);\n    // NIP-70 protected-event marker — prevents re-broadcasting by third parties.\n    tags.push(Tag::parse([\"-\"]).map_err(|e| anyhow::anyhow!(\"failed to build '-' tag: {e}\"))?);\n    for member in &members {\n        tags.push(\n            Tag::parse([\"member\", &member.pubkey, &member.role])\n                .map_err(|e| anyhow::anyhow!(\"failed to build member tag: {e}\"))?,\n        );\n    }\n\n    let event = EventBuilder::new(Kind::Custom(KIND_NIP43_MEMBERSHIP_LIST as u16), \"\")\n        .tags(tags)\n        .custom_created_at(nostr::Timestamp::from(ts))\n        .sign_with_keys(relay_keypair)\n        .map_err(|e| anyhow::anyhow!(\"failed to sign kind:13534: {e}\"))?;\n\n    let (stored, was_inserted) = db\n        .replace_addressable_event(tenant.community(), &event, None)\n        .await?;\n    if was_inserted {\n        // Publish to Redis so live clients receive the updated roster.\n        // Community-global scope (EventTopic::Global) matches the relay's own\n        // membership-list publish path; the tenant fixes the community.\n        if let Err(e) = pubsub\n            .publish_event(tenant, EventTopic::Global, &stored.event)","sourceCodeStart":348,"sourceCodeEnd":384,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/crates/buzz-admin/src/main.rs#L348-L384","documentation":"buzz-admin's add-member/remove-member path rebuilds the relay-signed kind:13534 (NIP-43 membership list) from the relay_members table, converting every row into a NIP-29 'member' tag with nostr's Tag::parse([\"member\", pubkey, role]). The nostr parser validates the second element as a Nostr pubkey (32-byte hex or bech32 npub); a malformed pubkey string or an unusable role value makes the parse return Err, which the CLI wraps as this anyhow error. The database write to relay_members has already succeeded at this point — only the roster republication fails.","triggerScenarios":"Running `buzz-admin add-member` or `buzz-admin remove-member` when list_relay_members returns a row whose pubkey is not 64 lowercase hex chars (wrong length, '0x' prefix, uppercase with mixed input, backslash-escaped BYTEA output copied from psql, or whitespace) or whose role string does not survive tag parsing.","commonSituations":"Seeding members with hand-written SQL instead of the CLI; storing pubkeys as raw bytes in one code path and hex in another (reconcile_channels hex-encodes m.pubkey, this path uses the string directly); pasting an npub with a newline or space into the DB.","solutions":["Find offending rows: SELECT pubkey, role FROM relay_members WHERE length(pubkey) <> 64 OR pubkey !~ '^[0-9a-f]{64}$';","Fix each bad row to a 64-char lowercase hex string (UPDATE relay_members SET pubkey = lower(hex_form) WHERE ...), or delete and re-add via buzz-admin add-member.","Verify role values are plain strings like owner/admin/member with no quotes or escapes.","Re-run the add-member/remove-member command so the kind:13534 roster is republished."],"exampleFix":"-- before: pubkey stored as escaped raw bytes (from a BYTEA copy) or wrong length\nSELECT pubkey FROM relay_members; -- '\\x8a0f...' or 40 chars\n-- after: canonical lowercase hex\nUPDATE relay_members SET pubkey = lower(regexp_replace(pubkey, '^\\\\x|[^0-9a-fA-F]', '', 'g')) WHERE pubkey !~ '^[0-9a-f]{64}$';\n-- if the value is unrecoverable, remove and re-add with the CLI:\n-- buzz-admin remove-member --pubkey <bad> ; buzz-admin add-member --pubkey <64-hex-npub-decoded> --role member","handlingStrategy":"validation","validationCode":"-- run before `buzz-admin add-member/remove-member`\nSELECT pubkey, role FROM relay_members\nWHERE pubkey !~ '^[0-9a-f]{64}$' OR role IS NULL OR role = '';\n-- zero rows means the roster rebuild will pass tag parsing;","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always create members through buzz-admin add-member instead of hand-written SQL so pubkeys are validated at write time.","Add a CI/nightly SQL assertion that relay_members.pubkey matches '^[0-9a-f]{64}$'.","Keep one storage convention: pubkeys in string columns are always lowercase 64-char hex; raw-byte columns are always 32 bytes."],"tags":["buzz-admin","nostr","member-tag","data-corruption","postgresql"],"backgroundTag":"malformed-pubkey","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-08-20T04:38:24.874Z","contentChangedAt":"2026-08-20T04:38:24.874Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}