tangle


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

commit c7196fb89707c4b6163c6c498f23874fe302a764
parent 2cef5dcfb67301716fa9cf8af0824e8c2848a9bc
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 04:07:55 -0700

runtime: remove JSON bridge from Pocket conversions

- Build Pocket events and filters from typed Tangle protocol fields instead of serializing through JSON.
- Convert Pocket events back to relay protocol events from Pocket fields without using as_json or parse_event_json.
- Screen query candidates with Pocket filter matching and the group EventView read gate before materializing relay events.
- Validated with cargo fmt --all -- --check, cargo check --workspace --all-targets, cargo test --workspace, and cargo clippy --workspace --all-targets -- -D warnings.

Diffstat:
Mcrates/tangle_runtime/src/groups.rs | 10++++------
Mcrates/tangle_runtime/src/pocket_conversion.rs | 242++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mcrates/tangle_runtime/src/relay/core.rs | 22+++++++++++++++++-----
Mcrates/tangle_runtime/tests/phase2_acceptance_targets.rs | 16++++++++++++++--
Mcrates/tangle_store_pocket/src/lib.rs | 9++++++++-
5 files changed, 271 insertions(+), 28 deletions(-)

diff --git a/crates/tangle_runtime/src/groups.rs b/crates/tangle_runtime/src/groups.rs @@ -2,7 +2,7 @@ use crate::{ errors::BaseRelayError, - pocket_conversion::{pocket_event_id, pocket_event_to_tangle, tangle_event_to_pocket}, + pocket_conversion::{pocket_event_id, tangle_event_to_pocket}, }; use std::str; use tangle_crypto::RelaySigner; @@ -14,8 +14,8 @@ use tangle_groups::{ KIND_GROUP_CREATE_GROUP, KIND_GROUP_DELETE_EVENT, KIND_GROUP_EDIT_METADATA, KIND_GROUP_JOIN_REQUEST, KIND_GROUP_LEAVE_REQUEST, KIND_GROUP_MEMBERS, KIND_GROUP_PUT_USER, KIND_GROUP_REMOVE_USER, MemberState, ProjectedRoleDefinition, ProjectionCheckpoint, - StoreOffset, event_deletion_key, group_current_key, member_current_key, - projection_checkpoint_key, role_current_key, tombstone_key, + StoreOffset, event_deletion_key, event_view::GroupEventView, group_current_key, + member_current_key, projection_checkpoint_key, role_current_key, tombstone_key, }; use tangle_protocol::{Event, EventId, PublicKeyHex, UnixTimestamp}; use tangle_store_pocket::{ @@ -115,8 +115,6 @@ impl GroupService { "delete target event is unavailable", )); }; - let target = pocket_event_to_tangle(&target) - .map_err(|error| GroupError::internal(error.prefixed_message()))?; let target_class = tangle_groups::classify_group_event(&target, self.limits)?; if target_class.group_id() != Some(group_id) { return Err(GroupError::invalid( @@ -129,7 +127,7 @@ impl GroupService { pub(crate) fn event_visible_to_auth( &self, - event: &Event, + event: &(impl GroupEventView + ?Sized), auth: &GroupAuthContext, ) -> Result<bool, GroupError> { let gate = GroupReadGate::new(&self.projection, &self.authority); diff --git a/crates/tangle_runtime/src/pocket_conversion.rs b/crates/tangle_runtime/src/pocket_conversion.rs @@ -2,32 +2,115 @@ use crate::errors::BaseRelayError; use std::str; -use tangle_protocol::{Event, EventId, Filter, event_to_value, filter_to_value, parse_event_json}; +use tangle_protocol::{ + Event, EventId, Filter, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, +}; use tangle_store_pocket::{ - PocketEvent, PocketEventId, PocketOwnedEvent, PocketOwnedFilter, parse_pocket_event_json, - parse_pocket_filter_json, + PocketEvent, PocketEventId, PocketKind, PocketOwnedEvent, PocketOwnedFilter, PocketOwnedTags, + PocketPubkey, PocketSig, PocketTags, PocketTime, }; pub(crate) fn tangle_event_to_pocket(event: &Event) -> Result<PocketOwnedEvent, BaseRelayError> { - let raw = event_to_value(event).to_string(); - parse_pocket_event_json(raw.as_bytes()).map_err(BaseRelayError::from) + let tags = tangle_tags_to_pocket(event.unsigned().tags())?; + ensure_event_size(tags.as_bytes().len(), event.unsigned().content().len())?; + PocketOwnedEvent::new( + pocket_event_id(event.id())?, + tangle_kind_to_pocket(event.unsigned().kind())?, + pocket_pubkey(event.unsigned().pubkey())?, + pocket_sig(event.sig())?, + &tags, + PocketTime::from_u64(event.unsigned().created_at().as_u64()), + event.unsigned().content().as_bytes(), + ) + .map_err(|error| BaseRelayError::error(error.to_string())) } pub(crate) fn tangle_filter_to_pocket( filter: &Filter, ) -> Result<PocketOwnedFilter, BaseRelayError> { - let raw = filter_to_value(filter).to_string(); - parse_pocket_filter_json(raw.as_bytes()).map_err(BaseRelayError::from) + let ids = filter + .ids() + .iter() + .map(pocket_event_id) + .collect::<Result<Vec<_>, _>>()?; + let authors = filter + .authors() + .iter() + .map(pocket_pubkey) + .collect::<Result<Vec<_>, _>>()?; + let kinds = filter + .kinds() + .iter() + .copied() + .map(tangle_kind_to_pocket) + .collect::<Result<Vec<_>, _>>()?; + let tag_parts = filter + .tag_filters() + .iter() + .map(|(name, values)| { + core::iter::once(name.as_str().to_owned()) + .chain(values.iter().map(|value| value.as_str().to_owned())) + .collect::<Vec<_>>() + }) + .collect::<Vec<_>>(); + ensure_filter_array_len("ids", ids.len())?; + ensure_filter_array_len("authors", authors.len())?; + ensure_filter_array_len("kinds", kinds.len())?; + ensure_tag_size(PocketTags::output_size_needed(&tag_parts))?; + let tags = PocketOwnedTags::new(&tag_parts) + .map_err(|error| BaseRelayError::error(error.to_string()))?; + let limit = filter + .limit() + .map(|limit| { + u32::try_from(limit) + .map_err(|_| BaseRelayError::invalid(format!("filter limit {limit} exceeds u32"))) + }) + .transpose()?; + ensure_filter_size(&ids, &authors, &kinds, &tags)?; + PocketOwnedFilter::new( + &ids, + &authors, + &kinds, + &tags, + filter + .since() + .map(|since| PocketTime::from_u64(since.as_u64())), + filter + .until() + .map(|until| PocketTime::from_u64(until.as_u64())), + limit, + ) + .map_err(|error| BaseRelayError::error(error.to_string())) } pub(crate) fn pocket_event_to_tangle(event: &PocketEvent) -> Result<Event, BaseRelayError> { - let raw = event - .as_json() - .map_err(|error| BaseRelayError::error(error.to_string()))?; - let raw = str::from_utf8(&raw).map_err(|error| BaseRelayError::error(error.to_string()))?; - let raw = tangle_protocol::RawEventJson::new(raw) + let tags = event + .tags() + .map_err(|error| BaseRelayError::error(error.to_string()))? + .iter() + .map(|tag| { + tag.map(|value| { + str::from_utf8(value) + .map(str::to_owned) + .map_err(|error| BaseRelayError::error(error.to_string())) + }) + .collect::<Result<Vec<_>, _>>() + .and_then(|values| Tag::new(values).map_err(BaseRelayError::error)) + }) + .collect::<Result<Vec<_>, _>>()?; + let content = str::from_utf8(event.content()) .map_err(|error| BaseRelayError::error(error.to_string()))?; - parse_event_json(&raw).map_err(|error| BaseRelayError::error(error.to_string())) + Ok(Event::new( + EventId::new(&event.id().as_hex_string()).map_err(BaseRelayError::error)?, + UnsignedEvent::new( + PublicKeyHex::new(&event.pubkey().as_hex_string()).map_err(BaseRelayError::error)?, + UnixTimestamp::new(event.created_at().as_u64()), + Kind::new(u64::from(event.kind().as_u16())).map_err(BaseRelayError::error)?, + tags, + content, + ), + SignatureHex::new(&event.sig().to_string()).map_err(BaseRelayError::error)?, + )) } pub(crate) fn pocket_event_id(event_id: &EventId) -> Result<PocketEventId, BaseRelayError> { @@ -35,9 +118,93 @@ pub(crate) fn pocket_event_id(event_id: &EventId) -> Result<PocketEventId, BaseR .map_err(|error| BaseRelayError::error(error.to_string())) } +fn pocket_pubkey(pubkey: &PublicKeyHex) -> Result<PocketPubkey, BaseRelayError> { + PocketPubkey::read_hex(pubkey.as_str().as_bytes()) + .map_err(|error| BaseRelayError::error(error.to_string())) +} + +fn pocket_sig(sig: &SignatureHex) -> Result<PocketSig, BaseRelayError> { + PocketSig::read_hex(sig.as_str().as_bytes()) + .map_err(|error| BaseRelayError::error(error.to_string())) +} + +fn tangle_kind_to_pocket(kind: Kind) -> Result<PocketKind, BaseRelayError> { + u16::try_from(kind.as_u32()) + .map(PocketKind::from_u16) + .map_err(|_| { + BaseRelayError::invalid(format!( + "event kind {} exceeds Pocket kind range", + kind.as_u32() + )) + }) +} + +fn tangle_tags_to_pocket(tags: &[Tag]) -> Result<PocketOwnedTags, BaseRelayError> { + let parts = tags + .iter() + .map(|tag| tag.values().iter().map(String::as_str).collect::<Vec<_>>()) + .collect::<Vec<_>>(); + ensure_tag_size(PocketTags::output_size_needed(&parts))?; + PocketOwnedTags::new(&parts).map_err(|error| BaseRelayError::error(error.to_string())) +} + +fn ensure_tag_size(size: usize) -> Result<(), BaseRelayError> { + if size > usize::from(u16::MAX) { + return Err(BaseRelayError::invalid(format!( + "tag section size {size} exceeds Pocket range" + ))); + } + Ok(()) +} + +fn ensure_filter_array_len(name: &str, len: usize) -> Result<(), BaseRelayError> { + if len > usize::from(u16::MAX) { + return Err(BaseRelayError::invalid(format!( + "filter {name} count {len} exceeds Pocket range" + ))); + } + Ok(()) +} + +fn ensure_filter_size( + ids: &[PocketEventId], + authors: &[PocketPubkey], + kinds: &[PocketKind], + tags: &PocketTags, +) -> Result<(), BaseRelayError> { + let size = tangle_store_pocket::PocketFilter::output_size_needed(ids, authors, kinds, tags); + if size > usize::try_from(u32::MAX).expect("u32 max fits usize") { + return Err(BaseRelayError::invalid(format!( + "filter size {size} exceeds Pocket range" + ))); + } + Ok(()) +} + +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!( + "event content size {content_len} exceeds Pocket range" + ))); + } + let size = PocketEvent::output_size_needed(tags_len, content_len); + if size > usize::try_from(u32::MAX).expect("u32 max fits usize") { + return Err(BaseRelayError::invalid(format!( + "event size {size} exceeds Pocket range" + ))); + } + Ok(()) +} + #[cfg(test)] mod tests { - use super::{pocket_event_id, pocket_event_to_tangle, tangle_event_to_pocket}; + use super::{ + pocket_event_id, pocket_event_to_tangle, tangle_event_to_pocket, tangle_filter_to_pocket, + }; + use tangle_protocol::{ + Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, + filter_from_value, + }; use tangle_test_support::{FixtureKey, tangle_v2_event}; #[test] @@ -50,4 +217,51 @@ mod tests { assert_eq!(converted, event); pocket_event_id(event.id()).expect("event id"); } + + #[test] + fn pocket_event_conversion_preserves_tags_and_utf8_content_without_json_bridge() { + let event = Event::new( + EventId::new(&"1".repeat(64)).expect("id"), + UnsignedEvent::new( + PublicKeyHex::new(&"2".repeat(64)).expect("pubkey"), + UnixTimestamp::new(1_714_124_433), + Kind::new(30_402).expect("kind"), + vec![ + Tag::from_parts("d", &["market"]).expect("d"), + Tag::from_parts("p", &[&"3".repeat(64), "relay"]).expect("p"), + ], + "harvest \u{2022} update", + ), + SignatureHex::new(&"4".repeat(128)).expect("sig"), + ); + let pocket = tangle_event_to_pocket(&event).expect("pocket"); + let converted = pocket_event_to_tangle(&pocket).expect("converted"); + + assert_eq!(converted, event); + } + + #[test] + fn pocket_filter_conversion_uses_native_filter_matching() { + let event = tangle_v2_event( + FixtureKey::Member, + 1_714_124_433, + 1, + vec![Tag::from_parts("t", &["market"]).expect("tag")], + "hello", + ) + .expect("event"); + let filter = filter_from_value(&serde_json::json!({ + "authors": [event.unsigned().pubkey().as_str()], + "kinds": [1], + "#t": ["market"], + "since": 1_714_124_400, + "limit": 1, + "search": "ignored by Pocket and Tangle matching" + })) + .expect("filter"); + let pocket_event = tangle_event_to_pocket(&event).expect("event"); + let pocket_filter = tangle_filter_to_pocket(&filter).expect("filter"); + + assert!(pocket_filter.event_matches(&pocket_event).expect("match")); + } } diff --git a/crates/tangle_runtime/src/relay/core.rs b/crates/tangle_runtime/src/relay/core.rs @@ -15,7 +15,7 @@ use tangle_groups::{ validate_client_group_event_structure, }; use tangle_protocol::{ClientMessage, Event, Filter, RelayMessage, SubscriptionId, UnixTimestamp}; -use tangle_store_pocket::{PocketScreenResult, PocketStoreConfig, PocketStoreHandle}; +use tangle_store_pocket::{PocketEvent, PocketScreenResult, PocketStoreConfig, PocketStoreHandle}; pub struct BaseRelay { store: PocketStoreHandle, @@ -477,9 +477,9 @@ impl BaseRelay { if screen_error.borrow().is_some() { return PocketScreenResult::Mismatch; } - match pocket_event_to_tangle(pocket_event) { - Ok(event) if !filter.matches(&event) => PocketScreenResult::Mismatch, - Ok(event) => match self.event_visible_to_auth(&event, auth) { + match pocket_filter.event_matches(pocket_event) { + Ok(false) => PocketScreenResult::Mismatch, + Ok(true) => match self.pocket_event_visible_to_auth(pocket_event, auth) { Ok(true) => PocketScreenResult::Match, Ok(false) => PocketScreenResult::Redacted, Err(error) => { @@ -488,7 +488,7 @@ impl BaseRelay { } }, Err(error) => { - *screen_error.borrow_mut() = Some(error); + *screen_error.borrow_mut() = Some(BaseRelayError::error(error.to_string())); PocketScreenResult::Mismatch } } @@ -531,6 +531,18 @@ impl BaseRelay { .map_err(BaseRelayError::from) } + fn pocket_event_visible_to_auth( + &self, + event: &PocketEvent, + auth: &GroupAuthContext, + ) -> Result<bool, BaseRelayError> { + self.groups + .as_ref() + .map(|groups| groups.event_visible_to_auth(event, auth)) + .unwrap_or(Ok(true)) + .map_err(BaseRelayError::from) + } + pub(crate) fn fanout_offset( &self, offset: StoreOffset, diff --git a/crates/tangle_runtime/tests/phase2_acceptance_targets.rs b/crates/tangle_runtime/tests/phase2_acceptance_targets.rs @@ -287,9 +287,21 @@ fn req_count_and_live_fanout_share_one_group_read_gate() { } #[test] -#[ignore = "phase2 target: hot path representation"] fn runtime_hot_path_does_not_stringify_and_reparse_events() { - pending("runtime hot paths must use Pocket event and filter types or EventView"); + let conversion_boundary = include_str!("../src/pocket_conversion.rs"); + for forbidden in [ + "event_to_value", + "filter_to_value", + "parse_event_json", + "parse_pocket_event_json", + "parse_pocket_filter_json", + ".as_json()", + ] { + assert!( + !conversion_boundary.contains(forbidden), + "runtime Pocket conversion boundary contains forbidden JSON bridge `{forbidden}`" + ); + } } #[test] diff --git a/crates/tangle_store_pocket/src/lib.rs b/crates/tangle_store_pocket/src/lib.rs @@ -5,7 +5,9 @@ use pocket_db::{ ScreenResult, Store, heed::{Database, types::Bytes}, }; -use pocket_types::{Event, Filter, Id, OwnedEvent, OwnedFilter, Pubkey}; +use pocket_types::{ + Event, Filter, Id, Kind, OwnedEvent, OwnedFilter, OwnedTags, Pubkey, Sig, Tags, Time, +}; use std::{ io, path::{Path, PathBuf}, @@ -17,9 +19,14 @@ pub const POCKET_SOURCE_REVISION: &str = "329334f20948c796c6016b673b92551ac4855a pub type PocketEvent = Event; pub type PocketEventId = Id; pub type PocketFilter = Filter; +pub type PocketKind = Kind; pub type PocketOwnedEvent = OwnedEvent; pub type PocketOwnedFilter = OwnedFilter; +pub type PocketOwnedTags = OwnedTags; pub type PocketPubkey = Pubkey; +pub type PocketSig = Sig; +pub type PocketTags = Tags; +pub type PocketTime = Time; pub type PocketScreenResult = ScreenResult; pub type PocketStore = Store; pub type PocketExtraRecord = (Vec<u8>, Vec<u8>);