{"record":{"id":"a67e7eadfd14f94c","repo":"block/buzz","slug":"moderation-notice-recipient-must-be-a-32-byte-pubk","errorCode":null,"errorMessage":"moderation notice recipient must be a 32-byte pubkey, got {}","messagePattern":"moderation notice recipient must be a 32-byte pubkey, got (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/buzz-relay/src/handlers/moderation_notices.rs","lineNumber":92,"sourceCode":"\n/// Deliver a moderation notice to `recipient` in this community's\n/// relay-authored DM thread (created on first use, reused after).\n///\n/// Idempotent and concurrency-safe: the notice event is constructed\n/// deterministically from `idempotency_ts` (the outbox row's `created_at`) so\n/// that two workers racing on the same outbox row produce byte-identical Nostr\n/// events. The `insert_event` ON CONFLICT DO NOTHING constraint then ensures\n/// exactly one row is durably persisted. Pass `row.created_at` as\n/// `idempotency_ts`.\npub async fn send_moderation_notice(\n    tenant: &TenantContext,\n    state: &Arc<AppState>,\n    recipient_pubkey: &[u8],\n    notice: ModerationNotice,\n    idempotency_ts: chrono::DateTime<chrono::Utc>,\n) -> anyhow::Result<()> {\n    if recipient_pubkey.len() != 32 {\n        anyhow::bail!(\n            \"moderation notice recipient must be a 32-byte pubkey, got {}\",\n            recipient_pubkey.len()\n        );\n    }\n    let relay_pubkey = state.relay_keypair.public_key();\n    let relay_pubkey_bytes = relay_pubkey.to_bytes();\n    let relay_pubkey_hex = hex::encode(relay_pubkey_bytes);\n\n    // Never DM the relay key itself (would create a self-DM and is meaningless).\n    if recipient_pubkey == relay_pubkey_bytes.as_slice() {\n        return Ok(());\n    }\n\n    // 1. Create/reuse the two-party DM channel {relay mod key, recipient}.\n    //    `open_dm` is participant-hash idempotent, so re-delivery to the same\n    //    user reuses the one thread per (community, user).\n    let (dm_channel, was_created) = state\n        .db","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/block/buzz/blob/eed74bde2f4797714335ac10c56c0b0244c1def4/crates/buzz-relay/src/handlers/moderation_notices.rs#L74-L110","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet pubkey = decode_pubkey(raw)?; // may yield non-32 bytes\nsend_moderation_notice(&state, &pubkey, notice, ts).await?;\n// after\nlet pubkey = decode_pubkey(raw)?;\nanyhow::ensure!(pubkey.len() == 32, \"invalid recipient pubkey length {}\", pubkey.len());\nsend_moderation_notice(&state, &pubkey, notice, ts).await?;","handlingStrategy":"validation","validationCode":"fn ensure_valid_pubkey(bytes: &[u8]) -> anyhow::Result<()> {\n    anyhow::ensure!(bytes.len() == 32, \"recipient pubkey must be 32 bytes, got {}\", bytes.len());\n    Ok(())\n}","typeGuard":"fn as_pubkey(bytes: &[u8]) -> Option<&[u8; 32]> {\n    bytes.try_into().ok()\n}","tryCatchPattern":"match send_moderation_notice(&state, recipient, notice, ts).await {\n  Err(e) if e.to_string().contains(\"must be a 32-byte pubkey\") => {\n    tracing::warn!(len = recipient.len(), \"skipping notice: invalid recipient pubkey\");\n  }\n  Err(e) => return Err(e),\n  Ok(v) => Ok(v),\n}","preventionTips":["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."],"tags":["nostr","validation","pubkey","backend","rust"],"backgroundTag":"invalid-public-key","analyzedSha":"eed74bde2f4797714335ac10c56c0b0244c1def4","analyzedAt":"2026-08-30T13:49:18.474Z","contentChangedAt":"2026-08-30T13:49:18.474Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}