tangle


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

commit 3e0d1de6407e9d28f9eb2fbd0c1aa5b65ab35686
parent de7e20d996f6558d40c2a24b177bbb0f49abe236
Author: triesap <tyson@radroots.org>
Date:   Tue, 16 Jun 2026 00:16:40 -0700

runtime: store generated Pocket events

- Store relay-generated group events as single Pocket-owned events.
- Remove protocol generated signing helpers from the group builder.
- Update generated recovery fixtures to materialize Pocket events directly.
- Gate protocol-to-Pocket conversion helpers to test-only adapter paths.

Diffstat:
MCargo.lock | 1+
Mcrates/tangle_groups/src/signing.rs | 152+++++++++++++++++++++++++++----------------------------------------------------
Mcrates/tangle_runtime/src/groups.rs | 66+++++++++++++++++++++++++++---------------------------------------
Mcrates/tangle_runtime/src/pocket_conversion.rs | 13++++++++++---
Mcrates/tangle_runtime/tests/base_relay_v2.rs | 6++----
Mcrates/tangle_test_support/Cargo.toml | 1+
Mcrates/tangle_test_support/src/lib.rs | 15+++++++++------
7 files changed, 102 insertions(+), 152 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1352,6 +1352,7 @@ name = "tangle_test_support" version = "0.1.0" dependencies = [ "k256", + "pocket-types", "serde_json", "tangle_crypto", "tangle_groups", diff --git a/crates/tangle_groups/src/signing.rs b/crates/tangle_groups/src/signing.rs @@ -10,7 +10,7 @@ use pocket_types::{ Time as PocketTime, }; use tangle_crypto::RelaySigner; -use tangle_protocol::{Event, Kind, PublicKeyHex, Tag, UnixTimestamp, UnsignedEvent}; +use tangle_protocol::{PublicKeyHex, UnixTimestamp}; pub struct GroupGeneratedEventBuilder { signer: RelaySigner, @@ -118,24 +118,6 @@ impl GroupGeneratedEventBuilder { membership_payload(KIND_GROUP_REMOVE_USER, group_id, target_pubkey, created_at) } - pub fn sign_payload(&self, payload: &GroupOutboxPayload) -> Result<Event, GroupError> { - let tags = payload - .tags() - .iter() - .cloned() - .map(Tag::new) - .collect::<Result<Vec<_>, _>>() - .map_err(GroupError::internal)?; - let unsigned = UnsignedEvent::new( - self.signer.public_key().clone(), - payload.generated_created_at(), - Kind::new(payload.generated_kind().into()).map_err(GroupError::internal)?, - tags, - payload.content(), - ); - Ok(self.signer.sign_unsigned_event(unsigned)) - } - pub fn sign_payload_pocket( &self, payload: &GroupOutboxPayload, @@ -163,52 +145,6 @@ impl GroupGeneratedEventBuilder { })?; Ok(event) } - - pub fn build_metadata_snapshot( - &self, - group: &GroupState, - created_at: UnixTimestamp, - ) -> Result<Event, GroupError> { - self.sign_payload(&Self::metadata_snapshot_payload(group, created_at)?) - } - - pub fn build_admin_list_snapshot( - &self, - group_id: &GroupId, - projection: &GroupProjection, - authority: &GroupAuthority, - created_at: UnixTimestamp, - ) -> Result<Event, GroupError> { - self.sign_payload(&Self::admin_list_snapshot_payload( - group_id, projection, authority, created_at, - )?) - } - - pub fn build_join_accepted( - &self, - group_id: &GroupId, - target_pubkey: &PublicKeyHex, - created_at: UnixTimestamp, - ) -> Result<Event, GroupError> { - self.sign_payload(&Self::join_accepted_payload( - group_id, - target_pubkey, - created_at, - )) - } - - pub fn build_leave_accepted( - &self, - group_id: &GroupId, - target_pubkey: &PublicKeyHex, - created_at: UnixTimestamp, - ) -> Result<Event, GroupError> { - self.sign_payload(&Self::leave_accepted_payload( - group_id, - target_pubkey, - created_at, - )) - } } fn metadata_tags( @@ -246,12 +182,16 @@ fn metadata_tags( tags.push(tag); } } - for tag in &tags { - Tag::new(tag.clone()).map_err(GroupError::internal)?; - } + validate_pocket_tags(&tags)?; Ok(tags) } +fn validate_pocket_tags(tags: &[Vec<String>]) -> Result<(), GroupError> { + PocketOwnedTags::new(tags).map(|_| ()).map_err(|error| { + GroupError::internal(format!("generated Pocket tags are invalid: {error}")) + }) +} + fn membership_payload( kind: u32, group_id: &GroupId, @@ -281,7 +221,8 @@ mod tests { KIND_GROUP_MEMBERS, KIND_GROUP_METADATA, KIND_GROUP_PUT_USER, KIND_GROUP_REMOVE_USER, MemberState, MemberStatus, ProjectionOrderTuple, StoreOffset, }; - use tangle_crypto::{RelaySigner, verify_event_signature}; + use pocket_types::Event as PocketEvent; + use tangle_crypto::RelaySigner; use tangle_protocol::{EventId, PublicKeyHex, UnixTimestamp}; #[test] @@ -289,13 +230,22 @@ mod tests { let builder = builder(); let group = group_state("Farm", GroupMetadata::empty()); let event = builder - .build_metadata_snapshot(&group, UnixTimestamp::new(20)) + .sign_payload_pocket( + &GroupGeneratedEventBuilder::metadata_snapshot_payload( + &group, + UnixTimestamp::new(20), + ) + .expect("payload"), + ) .expect("event"); - assert_eq!(event.unsigned().kind().as_u32(), KIND_GROUP_METADATA); - assert_eq!(event.unsigned().pubkey(), builder.relay_pubkey()); - assert!(has_tag(&event, &["d", "Farm"])); - verify_event_signature(&event).expect("signature"); + assert_eq!(u32::from(event.kind().as_u16()), KIND_GROUP_METADATA); + assert_eq!( + event.pubkey().as_hex_string(), + builder.relay_pubkey().as_str() + ); + assert!(has_pocket_tag(&event, &["d", "Farm"])); + event.verify().expect("signature"); } #[test] @@ -319,19 +269,22 @@ mod tests { ), ); let event = builder - .build_admin_list_snapshot( - &group_id, - &projection, - &GroupAuthority::new([owner.clone()], [admin.clone()]), - UnixTimestamp::new(20), + .sign_payload_pocket( + &GroupGeneratedEventBuilder::admin_list_snapshot_payload( + &group_id, + &projection, + &GroupAuthority::new([owner.clone()], [admin.clone()]), + UnixTimestamp::new(20), + ) + .expect("payload"), ) .expect("event"); - assert_eq!(event.unsigned().kind().as_u32(), KIND_GROUP_ADMINS); + assert_eq!(u32::from(event.kind().as_u16()), KIND_GROUP_ADMINS); for pubkey in [owner, admin, override_member] { - assert!(has_tag(&event, &["p", pubkey.as_str()])); + assert!(has_pocket_tag(&event, &["p", pubkey.as_str()])); } - verify_event_signature(&event).expect("signature"); + event.verify().expect("signature"); } #[test] @@ -376,18 +329,26 @@ mod tests { let group_id = GroupId::new("Farm").expect("group"); let member = pubkey("4"); let join = builder - .build_join_accepted(&group_id, &member, UnixTimestamp::new(20)) + .sign_payload_pocket(&GroupGeneratedEventBuilder::join_accepted_payload( + &group_id, + &member, + UnixTimestamp::new(20), + )) .expect("join"); let leave = builder - .build_leave_accepted(&group_id, &member, UnixTimestamp::new(21)) + .sign_payload_pocket(&GroupGeneratedEventBuilder::leave_accepted_payload( + &group_id, + &member, + UnixTimestamp::new(21), + )) .expect("leave"); - assert_eq!(join.unsigned().kind().as_u32(), KIND_GROUP_PUT_USER); - assert_eq!(leave.unsigned().kind().as_u32(), KIND_GROUP_REMOVE_USER); - for event in [join, leave] { - assert!(has_tag(&event, &["h", "Farm"])); - assert!(has_tag(&event, &["p", member.as_str()])); - verify_event_signature(&event).expect("signature"); + assert_eq!(u32::from(join.kind().as_u16()), KIND_GROUP_PUT_USER); + assert_eq!(u32::from(leave.kind().as_u16()), KIND_GROUP_REMOVE_USER); + for event in [&join, &leave] { + assert!(has_pocket_tag(event, &["h", "Farm"])); + assert!(has_pocket_tag(event, &["p", member.as_str()])); + event.verify().expect("signature"); } } @@ -507,16 +468,7 @@ mod tests { } } - fn has_tag(event: &tangle_protocol::Event, expected: &[&str]) -> bool { - event.unsigned().tags().iter().any(|tag| { - tag.values() - .iter() - .map(String::as_str) - .eq(expected.iter().copied()) - }) - } - - fn has_pocket_tag(event: &pocket_types::Event, expected: &[&str]) -> bool { + fn has_pocket_tag(event: &PocketEvent, expected: &[&str]) -> bool { event.tags().expect("tags").iter().any(|tag| { tag.map(|value| std::str::from_utf8(value).expect("tag")) .eq(expected.iter().copied()) diff --git a/crates/tangle_runtime/src/groups.rs b/crates/tangle_runtime/src/groups.rs @@ -1,9 +1,6 @@ #![forbid(unsafe_code)] -use crate::{ - errors::BaseRelayError, - pocket_conversion::{pocket_event_id, tangle_event_to_pocket}, -}; +use crate::{errors::BaseRelayError, pocket_conversion::pocket_event_id}; use std::{ ops::Deref, str, @@ -23,9 +20,11 @@ use tangle_groups::{ event_view::GroupEventView, group_current_key, member_current_key, projection_checkpoint_key, rebuild_group_projection, role_current_key, tombstone_key, }; -use tangle_protocol::{Event, EventId, PublicKeyHex, UnixTimestamp}; +#[cfg(test)] +use tangle_protocol::Event; +use tangle_protocol::{EventId, PublicKeyHex, UnixTimestamp}; use tangle_store_pocket::{ - PocketEvent, PocketOwnedEvent, PocketStoreHandle, TANGLE_GROUP_CHECKPOINT_TABLE, + PocketEvent, PocketEventId, PocketOwnedEvent, PocketStoreHandle, TANGLE_GROUP_CHECKPOINT_TABLE, TANGLE_GROUP_OUTBOX_TABLE, TANGLE_GROUP_PROJECTION_TABLE, }; @@ -45,8 +44,7 @@ pub(crate) enum GroupEventWriteError { } struct GeneratedGroupStorageEvent { - event: Event, - pocket_event: PocketOwnedEvent, + event: PocketOwnedEvent, } impl GeneratedGroupStorageEvent { @@ -54,20 +52,16 @@ impl GeneratedGroupStorageEvent { builder: &GroupGeneratedEventBuilder, payload: &GroupOutboxPayload, ) -> Result<Self, BaseRelayError> { - let event = builder.sign_payload(payload)?; - let pocket_event = tangle_event_to_pocket(&event)?; - Ok(Self { - event, - pocket_event, - }) + let event = builder.sign_payload_pocket(payload)?; + Ok(Self { event }) } - fn event(&self) -> &Event { + fn event(&self) -> &PocketEvent { &self.event } - fn pocket_event(&self) -> &PocketEvent { - &self.pocket_event + fn event_id(&self) -> Result<EventId, BaseRelayError> { + EventId::new(&self.event().id().as_hex_string()).map_err(BaseRelayError::error) } } @@ -310,7 +304,7 @@ impl GroupServiceState { { return Ok(GroupEventWrite::Duplicate); } - let pocket_event = tangle_event_to_pocket(event)?; + let pocket_event = crate::pocket_conversion::tangle_event_to_pocket(event)?; let store_offset = StoreOffset::new( store .store_event(&pocket_event) @@ -516,14 +510,15 @@ impl GroupServiceState { record: &GroupOutboxRecord, ) -> Result<(EventId, Option<StoreOffset>), BaseRelayError> { let generated = GeneratedGroupStorageEvent::build(&self.builder, record.payload())?; + let event_id = generated.event_id()?; if generated_event_already_stored(store, generated.event().id())? { - return Ok((generated.event().id().clone(), None)); + return Ok((event_id, None)); } - let offset = StoreOffset::new(store.store_event(generated.pocket_event())?); + let offset = StoreOffset::new(store.store_event(generated.event())?); self.projection .apply_canonical_event(generated.event(), offset, self.limits)?; self.persist_group_projection(store, record.key().group_id())?; - Ok((generated.event().id().clone(), Some(offset))) + Ok((event_id, Some(offset))) } fn persist_group_projection( @@ -1180,13 +1175,13 @@ fn persist_outbox_record( fn generated_event_already_stored( store: &PocketStoreHandle, - event_id: &EventId, + event_id: PocketEventId, ) -> Result<bool, BaseRelayError> { - if store.event_by_id(pocket_event_id(event_id)?)?.is_some() { + if store.event_by_id(event_id)?.is_some() { return Ok(true); } for stored in store.scan_events()? { - if stored.event().id().as_hex_string() == event_id.as_str() { + if stored.event().id() == event_id { return Ok(true); } } @@ -1235,7 +1230,6 @@ mod tests { validate_group_extra_tables, }; use crate::pocket_conversion::tangle_event_to_pocket; - use crate::pocket_event_validation::verify_pocket_event_signature; use tangle_crypto::RelaySigner; use tangle_groups::{ GroupGeneratedEventBuilder, GroupId, GroupRuntimeConfig, KIND_GROUP_METADATA, @@ -1265,26 +1259,20 @@ mod tests { let generated = GeneratedGroupStorageEvent::build(&builder, &payload).expect("generated"); assert_eq!( - generated.pocket_event().id().as_hex_string(), - generated.event().id().as_str() + generated.event().id().as_hex_string(), + generated.event_id().expect("event id").as_str() ); assert_eq!( - generated.pocket_event().pubkey().as_hex_string(), - generated.event().unsigned().pubkey().as_str() + generated.event().pubkey().as_hex_string(), + builder.relay_pubkey().as_str() ); assert_eq!( - u32::from(generated.pocket_event().kind().as_u16()), + u32::from(generated.event().kind().as_u16()), KIND_GROUP_PUT_USER ); - assert!(has_pocket_tag( - generated.pocket_event(), - &["h", "PocketFarm"] - )); - assert!(has_pocket_tag( - generated.pocket_event(), - &["p", member.as_str()] - )); - verify_pocket_event_signature(generated.pocket_event()).expect("signature"); + assert!(has_pocket_tag(generated.event(), &["h", "PocketFarm"])); + assert!(has_pocket_tag(generated.event(), &["p", member.as_str()])); + generated.event().verify().expect("signature"); } #[test] diff --git a/crates/tangle_runtime/src/pocket_conversion.rs b/crates/tangle_runtime/src/pocket_conversion.rs @@ -7,13 +7,14 @@ use tangle_protocol::Filter; use tangle_protocol::{ Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, }; +use tangle_store_pocket::{PocketEvent, PocketEventId}; #[cfg(test)] -use tangle_store_pocket::PocketOwnedFilter; use tangle_store_pocket::{ - PocketEvent, PocketEventId, PocketKind, PocketOwnedEvent, PocketOwnedTags, PocketPubkey, - PocketSig, PocketTags, PocketTime, + PocketKind, PocketOwnedEvent, PocketOwnedFilter, PocketOwnedTags, PocketPubkey, PocketSig, + PocketTags, PocketTime, }; +#[cfg(test)] pub(crate) fn tangle_event_to_pocket(event: &Event) -> Result<PocketOwnedEvent, BaseRelayError> { let tags = tangle_tags_to_pocket(event.unsigned().tags())?; ensure_event_size(tags.as_bytes().len(), event.unsigned().content().len())?; @@ -123,16 +124,19 @@ pub(crate) fn pocket_event_id(event_id: &EventId) -> Result<PocketEventId, BaseR .map_err(|error| BaseRelayError::error(error.to_string())) } +#[cfg(test)] pub(crate) fn pocket_pubkey(pubkey: &PublicKeyHex) -> Result<PocketPubkey, BaseRelayError> { PocketPubkey::read_hex(pubkey.as_str().as_bytes()) .map_err(|error| BaseRelayError::error(error.to_string())) } +#[cfg(test)] fn pocket_sig(sig: &SignatureHex) -> Result<PocketSig, BaseRelayError> { PocketSig::read_hex(sig.as_str().as_bytes()) .map_err(|error| BaseRelayError::error(error.to_string())) } +#[cfg(test)] fn tangle_kind_to_pocket(kind: Kind) -> Result<PocketKind, BaseRelayError> { u16::try_from(kind.as_u32()) .map(PocketKind::from_u16) @@ -144,6 +148,7 @@ fn tangle_kind_to_pocket(kind: Kind) -> Result<PocketKind, BaseRelayError> { }) } +#[cfg(test)] fn tangle_tags_to_pocket(tags: &[Tag]) -> Result<PocketOwnedTags, BaseRelayError> { let parts = tags .iter() @@ -153,6 +158,7 @@ fn tangle_tags_to_pocket(tags: &[Tag]) -> Result<PocketOwnedTags, BaseRelayError PocketOwnedTags::new(&parts).map_err(|error| BaseRelayError::error(error.to_string())) } +#[cfg(test)] fn ensure_tag_size(size: usize) -> Result<(), BaseRelayError> { if size > usize::from(u16::MAX) { return Err(BaseRelayError::invalid(format!( @@ -188,6 +194,7 @@ fn ensure_filter_size( Ok(()) } +#[cfg(test)] fn ensure_event_size(tags_len: usize, content_len: usize) -> Result<(), BaseRelayError> { if content_len > usize::try_from(u32::MAX).expect("u32 max fits usize") { return Err(BaseRelayError::invalid(format!( diff --git a/crates/tangle_runtime/tests/base_relay_v2.rs b/crates/tangle_runtime/tests/base_relay_v2.rs @@ -2431,11 +2431,9 @@ fn store_generated_events_for_pending_outbox_records( let builder = GroupGeneratedEventBuilder::new(signer); for record in records { let event = builder - .sign_payload(record.payload()) + .sign_payload_pocket(record.payload()) .expect("generated event"); - let raw = serde_json::to_vec(&event_to_value(&event)).expect("event JSON"); - let pocket = parse_pocket_event_json(&raw).expect("pocket event"); - store.store_event(&pocket).expect("store generated"); + store.store_event(&event).expect("store generated"); } store.sync().expect("sync"); } diff --git a/crates/tangle_test_support/Cargo.toml b/crates/tangle_test_support/Cargo.toml @@ -9,6 +9,7 @@ description = "Deterministic fixtures and event builders for tangle tests" [dependencies] k256 = { version = "0.13", features = ["schnorr"] } +pocket-types = { git = "https://github.com/triesap/pocket", rev = "329334f20948c796c6016b673b92551ac4855ad7" } serde_json = "1" tangle_crypto = { path = "../tangle_crypto" } tangle_groups = { path = "../tangle_groups" } diff --git a/crates/tangle_test_support/src/lib.rs b/crates/tangle_test_support/src/lib.rs @@ -3,6 +3,7 @@ use core::fmt; use k256::schnorr::signature::Signer; use k256::schnorr::{Signature, SigningKey}; +use pocket_types::OwnedEvent as PocketOwnedEvent; use tangle_crypto::{RelaySigner, compute_event_id}; use tangle_groups::{ CanonicalRelayUrl, GroupGeneratedEventBuilder, GroupLimitsConfig, GroupOutboxPayload, @@ -98,9 +99,11 @@ pub fn tangle_v2_relay_signer() -> Result<RelaySigner, String> { RelaySigner::from_secret_hex(TANGLE_V2_RELAY_SECRET_HEX).map_err(|error| error.to_string()) } -pub fn tangle_v2_generated_event(payload: &GroupOutboxPayload) -> Result<Event, String> { +pub fn tangle_v2_generated_pocket_event( + payload: &GroupOutboxPayload, +) -> Result<PocketOwnedEvent, String> { GroupGeneratedEventBuilder::new(tangle_v2_relay_signer()?) - .sign_payload(payload) + .sign_payload_pocket(payload) .map_err(|error| error.to_string()) } @@ -370,7 +373,7 @@ fn lower_hex(bytes: &[u8]) -> String { mod tests { use super::{ FixtureKey, build_fixture_event_from_parts, fixed_hex_bytes, fixture_event_json, - tangle_v2_auth_event, tangle_v2_generated_event, tangle_v2_group_config, + tangle_v2_auth_event, tangle_v2_generated_pocket_event, tangle_v2_group_config, tangle_v2_group_create_event, tangle_v2_group_event, tangle_v2_group_metadata_event, tangle_v2_join_event, tangle_v2_put_user_event, }; @@ -438,7 +441,7 @@ mod tests { vec![vec!["d".to_owned(), "Farm".to_owned()]], "", ); - let generated = tangle_v2_generated_event(&payload).expect("generated"); + let generated = tangle_v2_generated_pocket_event(&payload).expect("generated"); assert!(config.enabled()); assert_eq!(config.owner_pubkeys(), &[FixtureKey::Owner.public_key()]); @@ -447,8 +450,8 @@ mod tests { assert_eq!(verify_event_signature(&put), Ok(())); assert_eq!(verify_event_signature(&join), Ok(())); assert_eq!(verify_event_signature(&normal), Ok(())); - assert_eq!(verify_event_signature(&generated), Ok(())); - assert_eq!(generated.unsigned().kind().as_u32(), KIND_GROUP_METADATA); + generated.verify().expect("generated signature"); + assert_eq!(u32::from(generated.kind().as_u16()), KIND_GROUP_METADATA); } #[test]