tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 2++
Mcrates/tangle_crypto/Cargo.toml | 2++
Mcrates/tangle_crypto/src/lib.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
Mcrates/tangle_groups/src/signing.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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")) }