block/buzz · error · anyhow::Error
invalid relay key
Error message
invalid relay key: {e} What it means
reconcile-channels resolves its signing key from --relay-key or BUZZ_RELAY_PRIVATE_KEY and runs it through Keys::parse. The parser accepts only a 32-byte hex secret or a bech32 nsec; anything else — an npub public key, a truncated string, a quoted .env value, or a typo'd nsec — fails here. The key must be the SECRET key because the resulting kind:39000/39001/39002 events are attributed to the relay identity.
Solutions
- Use the secret: 64 hex chars or nsec…; verify length: echo -n "$key" | wc -c gives 64 for hex.
- Remove quotes/whitespace around the value in .env or the flag argument.
- If the relay key was lost, generate a new one and re-run reconcile for ALL channels (no --channel) so every snapshot shares the new identity.
- Check the chained {e} — 'bech32' errors mean a typo'd nsec; length errors mean truncation.
Example fix
# before buzz-admin reconcile-channels --relay-key npub1xyz... # public key, not secret # after buzz-admin reconcile-channels --relay-key nsec1... # secret key (hex also accepted)
Defensive patterns
Strategy: validation
Validate before calling
# validate before use: 64-hex or nsec, nothing else
RELAY_KEY="${RELAY_KEY:-$BUZZ_RELAY_PRIVATE_KEY}"
printf '%s' "$RELAY_KEY" | grep -qE '^([0-9a-fA-F]{64}|nsec1[a-z0-9]+)$' || { echo 'bad relay key' >&2; exit 3; } Type guard
fn parse_relay_key(raw: &str) -> Option<nostr::Keys> {
let s = raw.trim();
let ok = s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit())
|| s.starts_with("nsec1");
ok.then(|| nostr::Keys::parse(s).ok())?
} Prevention
- Keep exactly one canonical copy of the relay secret in your secret store; never retype it.
- Automate key handoff: read --relay-key from the secret manager in the run script, not from a pasted shell history.
- After any key rotation, re-run reconcile without --channel so all channel events share one identity.
When it happens
Trigger: Passing --relay-key <npub-or-wrong-length-string>, or BUZZ_RELAY_PRIVATE_KEY containing quotes/whitespace or a public key.
Common situations: Grabbing the relay's public key from logs because it is easier to find; deploy tooling wrapping env values in quotes; partial copy of a long hex string.
Related errors
- invalid BUZZ_RELAY_PRIVATE_KEY
- invalid --channel UUID
- sign kind:39000
- channel not found
- --channel requires --relay-key or BUZZ_RELAY_PRIVATE_KEY
AI-assisted analysis of block/buzz@ef2aa1ae38 (2026-08-20).
Data as JSON: /api/errors/c0136ceef17c61e3.
Report an issue: GitHub.
Appendix: source
Thrown at crates/buzz-admin/src/main.rs:643
) -> Result<()> {
use buzz_core::kind::KIND_NIP29_GROUP_ADMINS;
use buzz_db::event::EventQuery;
let db = connect_db().await?;
// Resolve relay signing key: arg > env > ephemeral. Force-republish must
// never use an ephemeral key because it replaces an existing authoritative
// snapshot.
let configured_relay_key =
relay_key_arg.or_else(|| std::env::var("BUZZ_RELAY_PRIVATE_KEY").ok());
if channel_arg.is_some() && configured_relay_key.is_none() {
return Err(anyhow::anyhow!(
"--channel requires --relay-key or BUZZ_RELAY_PRIVATE_KEY"
));
}
let relay_keys = match configured_relay_key {
Some(key_hex) => {
Keys::parse(&key_hex).map_err(|e| anyhow::anyhow!("invalid relay key: {e}"))?
}
None => {
let k = Keys::generate();
eprintln!(
"Warning: no relay key provided — using ephemeral key {}",
k.public_key().to_hex()
);
eprintln!("Events signed with this key won't be verifiable after this run.");
eprintln!("Pass --relay-key or set BUZZ_RELAY_PRIVATE_KEY for production use.");
k
}
};
let tenant = resolve_admin_tenant(&db).await?;
let target_channel = channel_arg
.as_deref()
.map(uuid::Uuid::parse_str)
.transpose()View on GitHub (pinned to ef2aa1ae38)