block/buzz · critical · anyhow::Error

invalid BUZZ_RELAY_PRIVATE_KEY: {e}

Error message

invalid BUZZ_RELAY_PRIVATE_KEY: {e}

What it means

When BUZZ_RELAY_PRIVATE_KEY is set, nostr::Keys::parse validates it as a secp256k1 secret key. Non-hex characters, wrong length (not 64 hex chars / 32 bytes), a 0x prefix, or embedded whitespace/newlines all fail parsing and abort startup. Note this only fires when the key IS set — the missing-key case is handled by the separate NIP-43/require-auth-token checks.

Source

Thrown at crates/buzz-relay/src/main.rs:427

        .read_database_url
        .as_deref()
        .unwrap_or(&config.database_url);
    let search_pool = sqlx::postgres::PgPoolOptions::new()
        .connect(search_db_url)
        .await
        .map_err(|e| anyhow::anyhow!("Search DB connection failed: {e}"))?;
    let search = SearchService::new(search_pool);
    info!(
        replica = config.read_database_url.is_some(),
        "Search service ready (Postgres FTS)"
    );

    let workflow_config = buzz_workflow::WorkflowConfig::default();
    let workflow_engine = Arc::new(WorkflowEngine::new(db.clone(), workflow_config));

    let relay_keypair = if let Some(hex) = &config.relay_private_key {
        nostr::Keys::parse(hex)
            .map_err(|e| anyhow::anyhow!("invalid BUZZ_RELAY_PRIVATE_KEY: {e}"))?
    } else if !config.require_auth_token {
        // Dev mode: use a deterministic keypair so addressable events (kind:39000/39001/39002)
        // replace correctly across restarts. Without this, each restart generates a new pubkey
        // and replace_addressable_event inserts duplicates instead of replacing.
        const DEV_RELAY_PRIVKEY: &str =
            "0000000000000000000000000000000000000000000000000000000000000001";
        let keys = nostr::Keys::parse(DEV_RELAY_PRIVKEY).expect("hardcoded dev key is valid");
        tracing::warn!(
            pubkey = %keys.public_key().to_hex(),
            "Using hardcoded dev relay keypair (BUZZ_REQUIRE_AUTH_TOKEN=false). \
             Set BUZZ_RELAY_PRIVATE_KEY for production."
        );
        keys
    } else {
        panic!(
            "BUZZ_RELAY_PRIVATE_KEY must be set when BUZZ_REQUIRE_AUTH_TOKEN=true. \
             A stable relay identity is required for production."
        );

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Set BUZZ_RELAY_PRIVATE_KEY to the raw 64-hex-character private key (the decoded nsec content, without the bech32 prefix)
  2. Strip whitespace: `tr -d '[:space:]'` when injecting from files
  3. Double-check you did not paste the pubkey

Example fix

# before
BUZZ_RELAY_PRIVATE_KEY=nsec1qw508d6qejxtdg4y5r3zarvary0c5xw7k

# after (hex body of the nsec, exactly 64 chars)
BUZZ_RELAY_PRIVATE_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
Defensive patterns

Strategy: validation

Validate before calling

# The relay key must be exactly 64 hex characters.
[[ "${BUZZ_RELAY_PRIVATE_KEY:-}" =~ ^[0-9a-fA-F]{64}$ ]] || { echo 'BUZZ_RELAY_PRIVATE_KEY must be 64 hex chars'; exit 1; }

Type guard

fn is_valid_secret_hex(s: &str) -> bool {
    s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
}

Prevention

When it happens

Trigger: Setting the key to an nsec1... bech32 string, base64, a 0x-prefixed or 66-char value, or a secret-store value with a trailing newline or surrounding quotes.

Common situations: Copy-pasting nsec1... from Nostr clients; secret managers appending newlines; pasting the public key where the private key belongs.

Related errors


AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16). Data as JSON: /api/errors/b620c2c4d4cc38df. Report an issue: GitHub.