block/buzz · error · IngestError::Rejected
invalid pubkey in a-tag
Error message
invalid pubkey in a-tag
What it means
The 'a'-tag deletion's second coordinate segment (the publisher pubkey) failed hex::decode. The relay decodes parts[1] as raw hex bytes to compare against the deletion's effective author, so any non-hex characters — an npub prefix, a bech32 string, a '0x' prefix, or a typo — make the coordinate unusable and the deletion is rejected before authorization even runs.
Source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:246
state: &Arc<AppState>,
) -> anyhow::Result<()> {
let actor_bytes = effective_message_author(event, &state.relay_keypair.public_key());
let target_ids = extract_target_event_ids(event);
if !has_e_tag(event) {
// a-tag deletion: verify author owns the addressable event
let a_tag = event
.tags
.iter()
.find(|t| t.kind().to_string() == "a")
.and_then(|t| t.content().map(|s| s.to_string()))
.ok_or_else(|| anyhow::anyhow!("missing e or a tag for target"))?;
let parts: Vec<&str> = a_tag.splitn(3, ':').collect();
if parts.len() < 2 {
return Err(anyhow::anyhow!("invalid a-tag format"));
}
let target_pubkey_bytes =
hex::decode(parts[1]).map_err(|_| anyhow::anyhow!("invalid pubkey in a-tag"))?;
if target_pubkey_bytes != actor_bytes
&& !state
.db
.is_agent_owner(tenant.community(), &target_pubkey_bytes, &actor_bytes)
.await?
{
return Err(anyhow::anyhow!("must be event author"));
}
return Ok(());
}
for target_id in target_ids {
let target_event = state
.db
.get_event_by_id_including_deleted(tenant.community(), &target_id)
.await?
.ok_or_else(|| anyhow::anyhow!("target event not found"))?;
View on GitHub (pinned to f956e6fe06)
Solutions
- Convert the key to 64-char lowercase hex before building the a tag (decode npub via NIP-19 first)
- Strip any 0x prefix and whitespace, then assert 64 hex characters with a regex ^[0-9a-f]{64}$
- Prefer SDK coordinate builders (e.g. Coordinate/EventCoordinate types) so the pubkey is formatted by the library
Example fix
// before
let a = format!("30078:{}:general", npub_string); // npub1... → invalid pubkey in a-tag
// after
let pk_hex = hex::encode(nip19::decode(&npub_string)?.1.to_bytes());
let a = format!("30078:{}:general", pk_hex); Defensive patterns
Strategy: validation
Validate before calling
// Normalize any key input to 64-char lowercase hex before it enters a tag
fn to_hex_pubkey(input: &str) -> Option<String> {
let s = input.trim().trim_start_matches("0x").to_ascii_lowercase();
match (s.len() == 64, s.chars().all(|c| c.is_ascii_hexdigit())) {
(true, true) => Some(s),
_ => match nip19::decode(input) { // accept npub and convert
Ok(_) => Some(hex::encode(nip19::decode(input).unwrap().1.to_bytes())),
Err(_) => None,
},
}
}
let pk = to_hex_pubkey(raw).ok_or("pubkey must be 64-char hex or npub")?; Type guard
const isHexPubkey = (v: string): boolean => /^[0-9a-f]{64}$/.test(v);
const toCoordinate = (kind: number, pk: string, d: string) =>
`${kind}:${toHex(pk)}:${d}`; Try / catch
match validate_standard_deletion_event(&tenant, &event, &state).await {
Err(e) if e.to_string().contains("invalid pubkey in a-tag") => {
warn_and_fix_coordinate(&event); // re-render with hex pubkey, re-sign, retry once
}
other => other,
} Prevention
- Store keys internally as hex; convert npub only at the display boundary
- Reject 0x prefixes and non-64-length strings at input parsing, not at publish time
- Use one shared normalize_key() helper everywhere tags are built so no call site forgets
When it happens
Trigger: Putting an npub/npub1... bech32 string where the hex pubkey belongs; prefixing with 0x; uppercase O/I or non-ASCII characters mixed into the hex; truncating the 64-char hex key.
Common situations: Client displays keys as npub and a developer pastes that form into the tag; key copied from a UI that inserted a space or ellipsis; NIP-19 conversion step skipped when building coordinates.
Related errors
- missing e or a tag for target
- invalid a-tag format
- target event not found
- must be event author
- missing or invalid h tag
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/69f319e1c04f36fc.
Report an issue: GitHub.