block/buzz · critical

failed to sign moderation notice: {e}

Error message

failed to sign moderation notice: {e}

What it means

After constructing the moderation notice event, the relay signs it with the relay's own keypair via `sign_with_keys`. This error wraps any failure from that signing operation (map_err into anyhow), meaning the notice event could not be signed and therefore was not stored or delivered.

Source

Thrown at crates/buzz-relay/src/handlers/moderation_notices.rs:175

    //    from `idempotency_ts` (the outbox row's immutable `created_at`). Two
    //    workers racing on the same outbox row produce byte-identical Nostr events
    //    (same pubkey + created_at + kind + tags + content = same SHA256 event ID).
    //    `insert_event`'s ON CONFLICT DO NOTHING ensures exactly one row is
    //    durably persisted regardless of how many workers reach this point.
    let source_id = notice.source_id();
    let tags = vec![
        Tag::parse(["h", &dm_channel_id.to_string()])?,
        Tag::parse([MODERATION_SOURCE_TAG, &source_id.to_string()])?,
    ];
    let ts = nostr::Timestamp::from(idempotency_ts.timestamp() as u64);
    let event = EventBuilder::new(
        Kind::Custom(KIND_STREAM_MESSAGE as u16),
        notice.body(tenant),
    )
    .tags(tags)
    .custom_created_at(ts)
    .sign_with_keys(&state.relay_keypair)
    .map_err(|e| anyhow::anyhow!("failed to sign moderation notice: {e}"))?;

    let (stored, _inserted) = state
        .db
        .insert_event(tenant.community(), &event, Some(dm_channel_id))
        .await?;

    let kind_u32 = event_kind_u32(&stored.event);
    dispatch_persistent_event(tenant, state, &stored, kind_u32, &relay_pubkey_hex, None).await;

    Ok(())
}

/// Publish the relay-signed kind:0 "{host} Moderation" profile so clients can
/// render the DM author with a recognizable name. Replaceable (NIP-01), so
/// re-emitting is idempotent — the latest wins.
async fn publish_moderation_profile(
    tenant: &TenantContext,
    state: &Arc<AppState>,

View on GitHub (pinned to eed74bde2f)

Solutions

  1. Check the relay keypair configuration (.env / key file) and confirm the secret key is a valid 32-byte secp256k1 scalar.
  2. Inspect the wrapped inner error message (`failed to sign moderation notice: {e}`) — it names the underlying secp256k1 cause.
  3. Regenerate or restore the relay identity key from a valid source and restart the relay.
  4. Add a startup check that loads and test-signs with the relay keypair so misconfiguration fails fast.

Example fix

// before — keypair loaded unchecked
let keypair = relay_keypair_from_env()?;
// after — fail fast at startup
let keypair = relay_keypair_from_env()?;
let probe = EventBuilder::new(Kind::Custom(0), "health-check");
probe.sign_with_keys(&keypair).context("relay keypair is unusable; fix BUZZ_RELAY_KEY")?;
Defensive patterns

Strategy: try-catch

Try / catch

match send_moderation_notice(&state, recipient, notice, ts).await {
  Err(e) if e.to_string().contains("failed to sign moderation notice") => {
    tracing::error!(cause = %e, "relay keypair unusable — halting notice delivery");
    // page an operator / fail startup rather than retrying blindly
  }
  Err(e) => return Err(e),
  Ok(v) => Ok(v),
}

Prevention

When it happens

Trigger: Calling `send_moderation_notice` when `state.relay_keypair` cannot produce a valid signature — corrupted or zeroed relay secret key, secp256k1 signing failure on the constructed event, or a keypair whose public key does not match the event's pubkey field.

Common situations: Relay identity key misconfigured (e.g. empty or invalid secret in env/config); key file corrupted or regenerated between restarts; cryptographic library failing on malformed key material at runtime; keypair loaded from a migration that produced a zero key.

Related errors


AI-assisted analysis of block/buzz@eed74bde2f (2026-08-30). Data as JSON: /api/errors/01ccfd0f0deee447. Report an issue: GitHub.