block/buzz · error · anyhow::Error
sign kind:39000
Error message
sign kind:39000: {e} What it means
reconcile-channels rebuilds the kind:39000 channel-metadata addressable event (d tag = channel UUID, plus public/hidden/closed/t tags) and signs it with relay_keys via sign_with_keys. All tags were already parsed successfully with Tag::parse(...)?, so a failure at signing comes from the nostr signing step itself: unusable key material or an event that fails to serialize. The {e} chain identifies the nostr Error variant.
Solutions
- Read the chained {e} — it names the exact nostr signing error.
- Re-verify the signing key: it must be the relay's 32-byte hex/nsec SECRET; re-sign a test event with it in a scratch script to isolate.
- Check channel_type and name columns for control characters or invalid UTF-8: SELECT channel_type, name FROM channels WHERE id = '<uuid>';
- Re-run reconcile after fixing the key or row data.
Defensive patterns
Strategy: try-catch
Validate before calling
// sign a scratch event with the same key first; if this fails, reconcile will too
let probe = nostr::EventBuilder::new(nostr::Kind::Custom(0), "probe")
.sign_with_keys(&relay_keys);
assert!(probe.is_ok(), "relay key cannot sign — fix before reconcile"); Try / catch
match reconcile_channel(&channel, &relay_keys).await {
Ok(_) => {}
Err(e) if e.to_string().contains("sign kind:39000") => {
// deterministic signing failure: log channel id + e.source() chain, skip channel, continue others
skipped.push((channel.id, e.to_string()));
}
Err(e) => return Err(e),
} Prevention
- Probe-sign once per run before iterating channels so one bad key stops the job before partial writes.
- Keep channel_type/name columns constrained to clean UTF-8 values.
- Always pass the stable relay key (--relay-key/env) for reconcile; treat the ephemeral path as bootstrap only.
When it happens
Trigger: The --relay-key/BUZZ_RELAY_PRIVATE_KEY value parsed as a Keys object but cannot produce a signature (corrupt secret material); or an event-serialization error from malformed tag content that survived parsing.
Common situations: Keys that pass a lenient parse but sign incorrectly after a bad key migration; exotic characters in channel_type ending up in the t tag.
Related errors
- failed to sign kind:13534
- invalid relay key
- sign kind:39001
- sign kind:39002
- --channel requires --relay-key or BUZZ_RELAY_PRIVATE_KEY
AI-assisted analysis of block/buzz@ef2aa1ae38 (2026-08-20).
Data as JSON: /api/errors/a75e4627e8ae756b.
Report an issue: GitHub.
Appendix: source
Thrown at crates/buzz-admin/src/main.rs:731
if !desc.is_empty() {
tags.push(Tag::parse(["about", desc])?);
}
}
if channel.visibility == "private" {
tags.push(Tag::parse(["private"])?);
} else {
tags.push(Tag::parse(["public"])?);
}
if channel.channel_type == "dm" {
tags.push(Tag::parse(["hidden"])?);
}
tags.push(Tag::parse(["closed"])?);
tags.push(Tag::parse(["t", &channel.channel_type])?);
let event = EventBuilder::new(Kind::Custom(39000), "")
.tags(tags)
.sign_with_keys(&relay_keys)
.map_err(|e| anyhow::anyhow!("sign kind:39000: {e}"))?;
db.replace_addressable_event(tenant.community(), &event, Some(channel.id))
.await?;
}
// kind:39001 — admins
{
let mut tags: Vec<Tag> = vec![Tag::parse(["d", &channel_id_str])?];
for m in members
.iter()
.filter(|m| m.role == "owner" || m.role == "admin")
{
let pk = hex::encode(&m.pubkey);
tags.push(Tag::parse(["p", &pk, &m.role])?);
}
let event = EventBuilder::new(Kind::Custom(KIND_NIP29_GROUP_ADMINS as u16), "")
.tags(tags)
.sign_with_keys(&relay_keys)
.map_err(|e| anyhow::anyhow!("sign kind:39001: {e}"))?;View on GitHub (pinned to ef2aa1ae38)