commit de7e20d996f6558d40c2a24b177bbb0f49abe236
parent 6b1ca6d8fb9c71194da3ff7268573b2e6edfb3be
Author: triesap <tyson@radroots.org>
Date: Tue, 16 Jun 2026 00:03:29 -0700
groups: sign generated Pocket events
- add RelaySigner support for Pocket-owned event signing
- build generated group payloads through Pocket tags
- verify generated Pocket events after signing
- cover generated NIP-29 events with stable golden ids
Diffstat:
4 files changed, 204 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1291,6 +1291,8 @@ name = "tangle_crypto"
version = "0.1.0"
dependencies = [
"k256",
+ "pocket-types",
+ "secp256k1",
"sha2",
"tangle_protocol",
"tokio",
diff --git a/crates/tangle_crypto/Cargo.toml b/crates/tangle_crypto/Cargo.toml
@@ -9,6 +9,8 @@ description = "Nostr event hashing and signature verification for tangle"
[dependencies]
k256 = { version = "0.13", features = ["schnorr"] }
+pocket-types = { git = "https://github.com/triesap/pocket", rev = "329334f20948c796c6016b673b92551ac4855ad7" }
+secp256k1 = "0.31"
sha2 = "0.10"
tangle_protocol = { path = "../tangle_protocol" }
tokio = { version = "1", features = ["macros", "rt", "sync", "time"] }
diff --git a/crates/tangle_crypto/src/lib.rs b/crates/tangle_crypto/src/lib.rs
@@ -5,6 +5,10 @@ use std::sync::Arc;
use k256::schnorr::signature::{Signer, Verifier};
use k256::schnorr::{Signature, SigningKey, VerifyingKey};
+use pocket_types::{
+ Kind as PocketKind, OwnedEvent as PocketOwnedEvent, Tags as PocketTags, Time as PocketTime,
+};
+use secp256k1::{Keypair, Secp256k1, SecretKey};
use sha2::{Digest, Sha256};
use tangle_protocol::{
Event, EventId, PublicKeyHex, SignatureHex, UnsignedEvent, canonical_event_json,
@@ -92,6 +96,7 @@ pub fn verify_event_signature_bytes(
pub struct RelaySigner {
signing_key: SigningKey,
public_key: PublicKeyHex,
+ secret_bytes: [u8; 32],
}
impl RelaySigner {
@@ -108,6 +113,7 @@ impl RelaySigner {
Ok(Self {
signing_key,
public_key,
+ secret_bytes: bytes,
})
}
@@ -124,6 +130,21 @@ impl RelaySigner {
.expect("schnorr signature emits valid hex");
Event::new(event_id, unsigned, signature)
}
+
+ pub fn sign_pocket_event(
+ &self,
+ kind: PocketKind,
+ tags: &PocketTags,
+ created_at: PocketTime,
+ content: &[u8],
+ ) -> Result<PocketOwnedEvent, String> {
+ let secp = Secp256k1::new();
+ let secret_key = SecretKey::from_byte_array(self.secret_bytes)
+ .map_err(|_| "relay secret is not a valid secp256k1 signing key".to_owned())?;
+ let keypair = Keypair::from_secret_key(&secp, &secret_key);
+ PocketOwnedEvent::sign_new(&keypair, kind, tags, created_at, content)
+ .map_err(|error| format!("Pocket event signing failed: {error}"))
+ }
}
impl fmt::Debug for RelaySigner {
@@ -236,6 +257,7 @@ mod tests {
};
use k256::schnorr::signature::Signer;
use k256::schnorr::{Signature, SigningKey};
+ use pocket_types::{Kind as PocketKind, OwnedTags as PocketOwnedTags, Time as PocketTime};
use std::time::Duration;
use tangle_protocol::{
Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent,
@@ -321,6 +343,29 @@ mod tests {
}
#[test]
+ fn relay_signer_signs_pocket_events() {
+ let secret = "7".repeat(64);
+ let signer = RelaySigner::from_secret_hex(&secret).expect("signer");
+ let tags = PocketOwnedTags::new(&[["d", "Farm"]]).expect("tags");
+ let event = signer
+ .sign_pocket_event(
+ PocketKind::from_u16(39_000),
+ &tags,
+ PocketTime::from_u64(20),
+ b"",
+ )
+ .expect("event");
+
+ event.verify().expect("verify");
+ assert_eq!(event.pubkey().as_hex_string(), signer.public_key().as_str());
+ assert_eq!(event.kind().as_u16(), 39_000);
+ assert_eq!(
+ event.id().as_hex_string(),
+ "b107997a285780bc383ee5aadc0a0eefc46734914103d80f765a46543622782a"
+ );
+ }
+
+ #[test]
fn schnorr_verifier_rejects_bad_id_bad_pubkey_and_bad_signature() {
let event = signed_event();
let wrong_id = Event::new(
diff --git a/crates/tangle_groups/src/signing.rs b/crates/tangle_groups/src/signing.rs
@@ -5,6 +5,10 @@ use crate::{
GroupState, KIND_GROUP_ADMINS, KIND_GROUP_MEMBERS, KIND_GROUP_METADATA, KIND_GROUP_PUT_USER,
KIND_GROUP_REMOVE_USER, MemberStatus, RoleName, SupportedKinds,
};
+use pocket_types::{
+ Kind as PocketKind, OwnedEvent as PocketOwnedEvent, OwnedTags as PocketOwnedTags,
+ Time as PocketTime,
+};
use tangle_crypto::RelaySigner;
use tangle_protocol::{Event, Kind, PublicKeyHex, Tag, UnixTimestamp, UnsignedEvent};
@@ -132,6 +136,34 @@ impl GroupGeneratedEventBuilder {
Ok(self.signer.sign_unsigned_event(unsigned))
}
+ pub fn sign_payload_pocket(
+ &self,
+ payload: &GroupOutboxPayload,
+ ) -> Result<PocketOwnedEvent, GroupError> {
+ let kind = PocketKind::from_u16(
+ u16::try_from(payload.generated_kind())
+ .map_err(|_| GroupError::internal("generated event kind exceeds Pocket kind"))?,
+ );
+ let tags = PocketOwnedTags::new(payload.tags()).map_err(|error| {
+ GroupError::internal(format!("generated Pocket tags are invalid: {error}"))
+ })?;
+ let event = self
+ .signer
+ .sign_pocket_event(
+ kind,
+ &tags,
+ PocketTime::from_u64(payload.generated_created_at().as_u64()),
+ payload.content().as_bytes(),
+ )
+ .map_err(GroupError::internal)?;
+ event.verify().map_err(|error| {
+ GroupError::internal(format!(
+ "generated Pocket event failed verification: {error}"
+ ))
+ })?;
+ Ok(event)
+ }
+
pub fn build_metadata_snapshot(
&self,
group: &GroupState,
@@ -359,6 +391,122 @@ mod tests {
}
}
+ #[test]
+ fn generated_pocket_events_have_stable_ids_and_verify() {
+ let builder = builder();
+ let group_id = GroupId::new("Farm").expect("group");
+ let group = group_state("Farm", GroupMetadata::empty());
+ let member = pubkey("4");
+ let owner = pubkey("1");
+ let admin = pubkey("2");
+ let metadata = builder
+ .sign_payload_pocket(
+ &GroupGeneratedEventBuilder::metadata_snapshot_payload(
+ &group,
+ UnixTimestamp::new(20),
+ )
+ .expect("payload"),
+ )
+ .expect("metadata");
+ let admins = builder
+ .sign_payload_pocket(
+ &GroupGeneratedEventBuilder::admin_list_snapshot_payload(
+ &group_id,
+ &GroupProjection::new(),
+ &GroupAuthority::new([owner.clone()], [admin.clone()]),
+ UnixTimestamp::new(20),
+ )
+ .expect("payload"),
+ )
+ .expect("admins");
+ let mut projection = GroupProjection::new();
+ projection.put_member(
+ group_id.clone(),
+ MemberState::new(
+ member.clone(),
+ MemberStatus::Member,
+ Default::default(),
+ event_id("30"),
+ tuple(30, "30", 3),
+ ),
+ );
+ let members = builder
+ .sign_payload_pocket(
+ &GroupGeneratedEventBuilder::member_list_snapshot_payload(
+ &group_id,
+ &projection,
+ UnixTimestamp::new(20),
+ 1,
+ )
+ .expect("payload")
+ .expect("members"),
+ )
+ .expect("members");
+ let join = builder
+ .sign_payload_pocket(&GroupGeneratedEventBuilder::join_accepted_payload(
+ &group_id,
+ &member,
+ UnixTimestamp::new(20),
+ ))
+ .expect("join");
+ let leave = builder
+ .sign_payload_pocket(&GroupGeneratedEventBuilder::leave_accepted_payload(
+ &group_id,
+ &member,
+ UnixTimestamp::new(21),
+ ))
+ .expect("leave");
+
+ for (event, kind, event_id, expected_tags) in [
+ (
+ metadata,
+ KIND_GROUP_METADATA,
+ "b107997a285780bc383ee5aadc0a0eefc46734914103d80f765a46543622782a",
+ vec![vec!["d", "Farm"]],
+ ),
+ (
+ admins,
+ KIND_GROUP_ADMINS,
+ "f7a2e2a721877794dbd367208eec08bd487cf1955ad60cb615ad77e67b0f66e3",
+ vec![
+ vec!["d", "Farm"],
+ vec!["p", owner.as_str()],
+ vec!["p", admin.as_str()],
+ ],
+ ),
+ (
+ members,
+ KIND_GROUP_MEMBERS,
+ "19aa593a5e6e34cda72286e75aef520c05b56eed07fdee71f0d63b3efee3f814",
+ vec![vec!["d", "Farm"], vec!["p", member.as_str()]],
+ ),
+ (
+ join,
+ KIND_GROUP_PUT_USER,
+ "fcea9360ebfcae11580ce179bffd235dbcdf8093c223986780c0635c9fd720e3",
+ vec![vec!["h", "Farm"], vec!["p", member.as_str()]],
+ ),
+ (
+ leave,
+ KIND_GROUP_REMOVE_USER,
+ "bcba4eb36d55752f9274bf8a3118822a5ac3479fdd23b86b592514c945bd7ee8",
+ vec![vec!["h", "Farm"], vec!["p", member.as_str()]],
+ ),
+ ] {
+ event.verify().expect("verify");
+ assert_eq!(event.id().as_hex_string(), event_id);
+ assert_eq!(u32::from(event.kind().as_u16()), kind);
+ assert_eq!(
+ event.pubkey().as_hex_string(),
+ builder.relay_pubkey().as_str()
+ );
+ assert_eq!(event.content(), b"");
+ for expected in expected_tags {
+ assert!(has_pocket_tag(&event, &expected));
+ }
+ }
+ }
+
fn has_tag(event: &tangle_protocol::Event, expected: &[&str]) -> bool {
event.unsigned().tags().iter().any(|tag| {
tag.values()
@@ -368,6 +516,13 @@ mod tests {
})
}
+ fn has_pocket_tag(event: &pocket_types::Event, expected: &[&str]) -> bool {
+ event.tags().expect("tags").iter().any(|tag| {
+ tag.map(|value| std::str::from_utf8(value).expect("tag"))
+ .eq(expected.iter().copied())
+ })
+ }
+
fn builder() -> GroupGeneratedEventBuilder {
GroupGeneratedEventBuilder::new(RelaySigner::from_secret_hex(&"7".repeat(64)).expect("key"))
}