block/buzz · error · anyhow::Error

failed to build member tag: {e}

Error message

failed to build member tag: {e}

What it means

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.

Source

Thrown at crates/buzz-admin/src/main.rs:366

        )
        .await?
        .map(|e| e.event.created_at.as_secs());

    // custom_created_at = max(now, existing + 1s) — defeats same-second domination.
    let ts = match newest_ts {
        Some(existing) => (existing + 1).max(now),
        None => now,
    };

    let members = db.list_relay_members(tenant.community()).await?;

    let mut tags: Vec<Tag> = Vec::with_capacity(members.len() + 1);
    // NIP-70 protected-event marker — prevents re-broadcasting by third parties.
    tags.push(Tag::parse(["-"]).map_err(|e| anyhow::anyhow!("failed to build '-' tag: {e}"))?);
    for member in &members {
        tags.push(
            Tag::parse(["member", &member.pubkey, &member.role])
                .map_err(|e| anyhow::anyhow!("failed to build member tag: {e}"))?,
        );
    }

    let event = EventBuilder::new(Kind::Custom(KIND_NIP43_MEMBERSHIP_LIST as u16), "")
        .tags(tags)
        .custom_created_at(nostr::Timestamp::from(ts))
        .sign_with_keys(relay_keypair)
        .map_err(|e| anyhow::anyhow!("failed to sign kind:13534: {e}"))?;

    let (stored, was_inserted) = db
        .replace_addressable_event(tenant.community(), &event, None)
        .await?;
    if was_inserted {
        // Publish to Redis so live clients receive the updated roster.
        // Community-global scope (EventTopic::Global) matches the relay's own
        // membership-list publish path; the tenant fixes the community.
        if let Err(e) = pubsub
            .publish_event(tenant, EventTopic::Global, &stored.event)

View on GitHub (pinned to dad5a33865)

Solutions

  1. Find offending rows: SELECT pubkey, role FROM relay_members WHERE length(pubkey) <> 64 OR pubkey !~ '^[0-9a-f]{64}$';
  2. 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.
  3. Verify role values are plain strings like owner/admin/member with no quotes or escapes.
  4. Re-run the add-member/remove-member command so the kind:13534 roster is republished.

Example fix

-- before: pubkey stored as escaped raw bytes (from a BYTEA copy) or wrong length
SELECT pubkey FROM relay_members; -- '\x8a0f...' or 40 chars
-- after: canonical lowercase hex
UPDATE relay_members SET pubkey = lower(regexp_replace(pubkey, '^\\x|[^0-9a-fA-F]', '', 'g')) WHERE pubkey !~ '^[0-9a-f]{64}$';
-- if the value is unrecoverable, remove and re-add with the CLI:
-- buzz-admin remove-member --pubkey <bad> ; buzz-admin add-member --pubkey <64-hex-npub-decoded> --role member
Defensive patterns

Strategy: validation

Validate before calling

-- run before `buzz-admin add-member/remove-member`
SELECT pubkey, role FROM relay_members
WHERE pubkey !~ '^[0-9a-f]{64}$' OR role IS NULL OR role = '';
-- zero rows means the roster rebuild will pass tag parsing;

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of block/buzz@dad5a33865 (2026-08-20). Data as JSON: /api/errors/54ebe9c94bc6c27c. Report an issue: GitHub.