block/buzz · error · anyhow::Error
missing p tag
Error message
missing p tag
What it means
For kind 9000 (PUT_USER) admin events, validate_admin_event extracts the target member from the event's `p` tag via extract_p_tag. If the tag is absent it fails with 'missing p tag'. The p tag identifies which member the role/membership mutation applies to.
Solutions
- Add a `p` tag containing the target member's 32-byte hex pubkey to the kind:9000 event
- Validate event shape client-side before publishing (required tags per kind)
- Check the client library's PUT_USER helper is actually attaching the tag
Example fix
// before
let ev = EventBuilder::new(9000, "").tag(Tag::custom("h", [channel_id]));
// after
let ev = EventBuilder::new(9000, "")
.tag(Tag::custom("h", [channel_id]))
.tag(Tag::public_key(target_pubkey)); // p tag: mutation target Defensive patterns
Strategy: validation
Validate before calling
// validate before publishing kind:9000
const p = event.tags.find(t => t[0] === 'p');
if (!p || !/^[0-9a-f]{64}$/.test(p[1])) {
throw new Error('kind:9000 requires a p tag with a 32-byte hex pubkey');
} Type guard
function hasPTag(tags: string[][]): tags is [string[], ...string[][]] & { p: string } {
return tags.some(t => t[0] === 'p' && /^[0-9a-f]{64}$/.test(t[1] ?? ''));
} Try / catch
match publish(put_user_event).await {
Err(e) if e.to_string().contains("missing p tag") => fix_and_republish_with_p_tag(),
other => other?,
} Prevention
- Encode required-tags-per-kind in the client event builder so they cannot be omitted
- Add unit tests asserting PUT_USER events always carry a p tag
- Validate tag shape (hex pubkey) before signing
When it happens
Trigger: A kind:9000 channel PUT_USER event published without a `p` tag (or with an empty/malformed one that extract_p_tag rejects).
Common situations: A bot or script constructs the admin event manually and forgets the p tag; a client bug after a UI change drops the target member tag; hand-crafted events during interop testing.
Related errors
- invalid role
- channel not found
- failed to build setup nudge
- moderation notice recipient must be a 32-byte pubkey, got
- bundled model-capabilities.json must parse
AI-assisted analysis of block/buzz@dad5a33865 (2026-09-05).
Data as JSON: /api/errors/5266242998b86b62.
Report an issue: GitHub.
Appendix: source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:367
// 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"))?;
// Authorization policy — visibility gate, elevated-grant gate,
// active-member role-change gate, and last-owner demotion — lives in
// `channel_authz`, which is pure and table-tested. The database reads
// it depends on stay here.
match channel_authz::decide_put_user(
&channel.visibility,
actor_role,
requested_role,
&members,
&target_pubkey,
&actor_bytes,
)? {
// Self-add: always allowed regardless of policy.
PutUserDecision::Allow => Ok(()),
// Third-party add: check channel_add_policy on the target.
PutUserDecision::CheckAddPolicy => {
if let Some((policy, owner)) = stateView on GitHub (pinned to dad5a33865)