block/buzz · error
moderation notice recipient must be a 32-byte pubkey, got {}
Error message
moderation notice recipient must be a 32-byte pubkey, got {} What it means
`send_moderation_notice` in buzz-relay validates that the notice recipient is exactly 32 bytes — the canonical size of a Nostr/secp256k1 public key — before building and signing the notice event. A non-32-byte slice means a malformed or truncated pubkey was passed in from an upstream handler (report resolution, ban, timeout), and the relay refuses to send a DM to an invalid key.
Source
Thrown at crates/buzz-relay/src/handlers/moderation_notices.rs:92
/// Deliver a moderation notice to `recipient` in this community's
/// relay-authored DM thread (created on first use, reused after).
///
/// Idempotent and concurrency-safe: the notice event is constructed
/// deterministically from `idempotency_ts` (the outbox row's `created_at`) so
/// that two workers racing on the same outbox row produce byte-identical Nostr
/// events. The `insert_event` ON CONFLICT DO NOTHING constraint then ensures
/// exactly one row is durably persisted. Pass `row.created_at` as
/// `idempotency_ts`.
pub async fn send_moderation_notice(
tenant: &TenantContext,
state: &Arc<AppState>,
recipient_pubkey: &[u8],
notice: ModerationNotice,
idempotency_ts: chrono::DateTime<chrono::Utc>,
) -> anyhow::Result<()> {
if recipient_pubkey.len() != 32 {
anyhow::bail!(
"moderation notice recipient must be a 32-byte pubkey, got {}",
recipient_pubkey.len()
);
}
let relay_pubkey = state.relay_keypair.public_key();
let relay_pubkey_bytes = relay_pubkey.to_bytes();
let relay_pubkey_hex = hex::encode(relay_pubkey_bytes);
// Never DM the relay key itself (would create a self-DM and is meaningless).
if recipient_pubkey == relay_pubkey_bytes.as_slice() {
return Ok(());
}
// 1. Create/reuse the two-party DM channel {relay mod key, recipient}.
// `open_dm` is participant-hash idempotent, so re-delivery to the same
// user reuses the one thread per (community, user).
let (dm_channel, was_created) = state
.dbView on GitHub (pinned to eed74bde2f)
Solutions
- Hex-decode the recipient pubkey and assert it is 64 hex chars (32 bytes) before calling any moderation notice function.
- Log the offending value length at the call site and fix the producer of the malformed pubkey (DB row, event tag, or API payload).
- Strip any 'npub'/'0x' prefixes and use a strict bech32/hex decoder that errors instead of truncating.
- Add a length assertion in the upstream handler when the pubkey is first parsed.
Example fix
// before
let pubkey = decode_pubkey(raw)?; // may yield non-32 bytes
send_moderation_notice(&state, &pubkey, notice, ts).await?;
// after
let pubkey = decode_pubkey(raw)?;
anyhow::ensure!(pubkey.len() == 32, "invalid recipient pubkey length {}", pubkey.len());
send_moderation_notice(&state, &pubkey, notice, ts).await?; Defensive patterns
Strategy: validation
Validate before calling
fn ensure_valid_pubkey(bytes: &[u8]) -> anyhow::Result<()> {
anyhow::ensure!(bytes.len() == 32, "recipient pubkey must be 32 bytes, got {}", bytes.len());
Ok(())
} Type guard
fn as_pubkey(bytes: &[u8]) -> Option<&[u8; 32]> {
bytes.try_into().ok()
} Try / catch
match send_moderation_notice(&state, recipient, notice, ts).await {
Err(e) if e.to_string().contains("must be a 32-byte pubkey") => {
tracing::warn!(len = recipient.len(), "skipping notice: invalid recipient pubkey");
}
Err(e) => return Err(e),
Ok(v) => Ok(v),
} Prevention
- Always hex-decode pubkeys with a strict decoder and check the result is 32 bytes.
- Strip npub/0x prefixes before decoding.
- Store pubkeys in fixed 32-byte columns (BYTEA(32) / CHAR(64) hex) to prevent truncation.
- Validate pubkeys at the API/event boundary before persisting them for enforcement.
When it happens
Trigger: Calling `send_moderation_notice` (directly or via `deliver_reporter_notice`, `deliver_affected_user_notice`, `handle_ban`, `handle_timeout`, `resolve_report_decision_only`) with a `recipient_pubkey` byte slice whose length is not 32 — e.g. a hex-decoded value of wrong length, an empty slice, or a 33-byte compressed key.
Common situations: A database row storing a truncated or corrupted pubkey; decoding a hex string with an odd number of characters or extra prefix ('0x' or 'npub' not stripped); a caller passing a slice of the wrong type (e.g. x-only vs full key) from another subsystem.
Related errors
- failed to sign moderation notice: {e}
- failed to build setup nudge: {e}
- channel not found
- missing p tag
- invalid role: {role_str}
AI-assisted analysis of block/buzz@eed74bde2f (2026-08-30).
Data as JSON: /api/errors/a67e7eadfd14f94c.
Report an issue: GitHub.