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:
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]