From 04c9e256a02c47a201e09b01ad83a97dd22d9e93 Mon Sep 17 00:00:00 2001 From: iroiro147 Date: Sun, 2 Aug 2026 20:09:54 +0530 Subject: [PATCH] fix: preserve self-referential p tags in channel member builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nostr 0.44's EventBuilder::build strips p tags whose value equals the author's public key by default (see allow_self_tagging flag). The build_add_member (kind 9000) and build_remove_member (kind 9001) builders constructed the p tag correctly, but sign_with_keys would then strip it when target_pubkey == signer pubkey, causing the relay to reply 400 "missing p tag" for self-removal/self-add operations. Fix by chaining .allow_self_tagging() on both builders, matching the existing convention in NIP-IA builders (builders.rs:1816,1837). Add regression tests that sign with the same key as the target pubkey and assert the p tag survives — the existing happy-path tests only exercised the non-self case, which is why the bug shipped. Closes #4326. Signed-off-by: Sarthak Singh --- crates/buzz-sdk/src/builders.rs | 45 +++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/buzz-sdk/src/builders.rs b/crates/buzz-sdk/src/builders.rs index 8cc9c8650a9..a6e83f19687 100644 --- a/crates/buzz-sdk/src/builders.rs +++ b/crates/buzz-sdk/src/builders.rs @@ -575,7 +575,9 @@ pub fn build_add_member( if let Some(r) = role { tags.push(tag(&["role", r.as_str()])?); } - Ok(EventBuilder::new(Kind::Custom(9000), "").tags(tags)) + Ok(EventBuilder::new(Kind::Custom(9000), "") + .tags(tags) + .allow_self_tagging()) } /// Build a NIP-29 remove-member event (kind 9001). @@ -588,7 +590,9 @@ pub fn build_remove_member( tag(&["h", &channel_id.to_string()])?, tag(&["p", &target_pubkey.to_ascii_lowercase()])?, ]; - Ok(EventBuilder::new(Kind::Custom(9001), "").tags(tags)) + Ok(EventBuilder::new(Kind::Custom(9001), "") + .tags(tags) + .allow_self_tagging()) } /// Build a NIP-29 leave-request event (kind 9022). @@ -2398,6 +2402,43 @@ mod tests { assert!(has_tag(&ev, "p", pubkey)); } + #[test] + fn remove_member_self_referential_preserves_p_tag() { + // Regression test for #4326: nostr 0.44 strips p-tags whose value + // equals the signer's pubkey unless `.allow_self_tagging()` is set. + let cid = uuid(); + let signer = keys(); + let signer_pubkey_hex = signer.public_key().to_hex(); + let ev = build_remove_member(cid, &signer_pubkey_hex) + .unwrap() + .sign_with_keys(&signer) + .expect("sign"); + assert_eq!(ev.kind.as_u16(), 9001); + assert!( + has_tag(&ev, "p", &signer_pubkey_hex), + "p tag targeting the signer must survive signing" + ); + assert!(has_tag(&ev, "h", &cid.to_string())); + } + + #[test] + fn add_member_self_referential_preserves_p_tag() { + // Same self-referential guarantee as remove_member (#4326). + let cid = uuid(); + let signer = keys(); + let signer_pubkey_hex = signer.public_key().to_hex(); + let ev = build_add_member(cid, &signer_pubkey_hex, Some(MemberRole::Member)) + .unwrap() + .sign_with_keys(&signer) + .expect("sign"); + assert_eq!(ev.kind.as_u16(), 9000); + assert!( + has_tag(&ev, "p", &signer_pubkey_hex), + "p tag targeting the signer must survive signing" + ); + assert!(has_tag(&ev, "role", "member")); + } + #[test] fn leave_happy_path() { let cid = uuid();