block/buzz · error · IngestError::Rejected
missing p tag
Error message
missing p tag
What it means
A kind 9000 PUT_USER event had no usable 'p' tag: extract_p_tag() requires a p tag whose content hex-decodes to exactly 32 bytes, and none matched. The p tag names the member being added/updated, so without it the relay cannot tell whose membership to mutate and rejects the event before storage.
Source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:359
// member that preserves the role they already hold, and only defaults
// to Member for a genuinely new member. Defaulting unconditionally to
// Member made a bare self-targeted PUT_USER silently demote an owner.
let role_str = extract_tag_value(event, "role");
let requested_role = match role_str {
Some(ref s) => match s.parse::<buzz_db::channel::MemberRole>() {
Ok(r) => Some(r),
Err(_) => return Err(anyhow::anyhow!("invalid role: {s}")),
},
None => None,
};
let members = state.db.get_members(tenant.community(), channel_id).await?;
let actor_role: Option<buzz_db::channel::MemberRole> = members
.iter()
.find(|m| m.pubkey == actor_bytes)
.and_then(|m| m.role.parse().ok());
let target_pubkey =
extract_p_tag(event).ok_or_else(|| anyhow::anyhow!("missing p tag"))?;
// PUT_USER: open channels allow any authenticated user; private channels
// require the actor to be an existing active member. Any active member may
// add an ordinary member, guest, or bot, but only owners/admins may grant
// an elevated role.
if channel.visibility == "private" {
if actor_role.is_none() {
return Err(anyhow::anyhow!("actor not authorized"));
}
if requested_role.is_some_and(|role| role.is_elevated())
&& !actor_role.is_some_and(|role| role.is_elevated())
{
return Err(anyhow::anyhow!(
"only owners/admins may grant elevated roles"
));
}
}View on GitHub (pinned to f956e6fe06)
Solutions
- Add ["p", "<64-char hex pubkey>"] as the target of the membership change
- Convert npub → hex before building the tag (NIP-19 decode)
- Ensure the p tag is the first p tag and its value round-trips: hex::decode(...).len() == 32
Example fix
// before
EventBuilder::new(Kind::from(9000), "", [
Tag::custom(TagKind::Custom("h"), vec![channel_uuid]),
Tag::custom(TagKind::Custom("role"), vec!["member"]),
Tag::custom(TagKind::Custom("p"), vec![npub_string]), // bech32, not hex
])
// after
let pk_hex = hex::encode(nip19::decode(&npub_string)?.1.to_bytes());
Tag::custom(TagKind::Custom("p"), vec![pk_hex]) Defensive patterns
Strategy: validation
Validate before calling
// Mirror extract_p_tag before publishing a 9000
fn valid_p_tag(tags: &[Tag]) -> bool {
tags.iter().any(|t| {
t.kind().to_string() == "p"
&& t.content()
.and_then(|v| hex::decode(v).ok())
.map(|b| b.len() == 32)
.unwrap_or(false)
})
}
assert!(valid_p_tag(&event.tags), "PUT_USER needs p = 64-hex pubkey"); Type guard
const isValidPTag = (tags: string[][]): boolean =>
tags.some(t => t[0] === "p" && /^[0-9a-f]{64}$/.test(t[1] ?? "")); Try / catch
match validate_admin_event(&tenant, 9000, &event, &state).await {
Err(e) if e.to_string().contains("missing p tag") => {
reject("PUT_USER requires [\"p\", \"<64-hex pubkey>\"]", &event.id)
}
other => other,
} Prevention
- Convert all user keys to hex at input; never paste npub strings into tag builders
- Make the target user a required parameter of your put_user builder so it cannot be omitted
- Remember the first p tag wins for extraction — keep the target as the only/first p tag
When it happens
Trigger: PUT_USER published with only h and role tags; p tag value is an npub (bech32) instead of 64-char hex; p tag hex is truncated (not 32 bytes) or has a 0x prefix; tag kind serialized as 'P' (uppercase); multiple p tags where the first is malformed — extraction stops at the first p tag and returns None if it fails.
Common situations: Pasting NIP-19 npub strings from a UI into tag builders; key strings trimmed/corrupted in transit; fixtures reusing a placeholder like "deadbeef" that is not 64 hex chars.
Related errors
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/5266242998b86b62.
Report an issue: GitHub.