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:
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>);