commit cc8c614a33051151a58fa7722f36648b781ba732
parent 7d173dc8bd37dc7fc6aedbb451541a289c974cc5
Author: triesap <tyson@radroots.org>
Date: Mon, 15 Jun 2026 20:33:38 -0700
runtime: isolate generated group Pocket storage
- Add an explicit generated group storage adapter that signs payloads and stores Pocket events.
- Keep projection application at the existing group boundary while avoiding a broad signer rewrite.
- Verify generated Pocket event id, tags, and signature at the adapter boundary.
- Validate generated, group, signing, workspace check, source scan, and clippy lanes.
Diffstat:
1 file changed, 88 insertions(+), 15 deletions(-)
diff --git a/crates/tangle_runtime/src/groups.rs b/crates/tangle_runtime/src/groups.rs
@@ -25,8 +25,8 @@ use tangle_groups::{
};
use tangle_protocol::{Event, EventId, PublicKeyHex, UnixTimestamp};
use tangle_store_pocket::{
- PocketEvent, PocketStoreHandle, TANGLE_GROUP_CHECKPOINT_TABLE, TANGLE_GROUP_OUTBOX_TABLE,
- TANGLE_GROUP_PROJECTION_TABLE,
+ PocketEvent, PocketOwnedEvent, PocketStoreHandle, TANGLE_GROUP_CHECKPOINT_TABLE,
+ TANGLE_GROUP_OUTBOX_TABLE, TANGLE_GROUP_PROJECTION_TABLE,
};
#[derive(Clone)]
@@ -44,6 +44,33 @@ pub(crate) enum GroupEventWriteError {
Storage(BaseRelayError),
}
+struct GeneratedGroupStorageEvent {
+ event: Event,
+ pocket_event: PocketOwnedEvent,
+}
+
+impl GeneratedGroupStorageEvent {
+ fn build(
+ 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,
+ })
+ }
+
+ fn event(&self) -> &Event {
+ &self.event
+ }
+
+ fn pocket_event(&self) -> &PocketEvent {
+ &self.pocket_event
+ }
+}
+
impl From<BaseRelayError> for GroupEventWriteError {
fn from(error: BaseRelayError) -> Self {
Self::Storage(error)
@@ -494,16 +521,15 @@ impl GroupServiceState {
store: &PocketStoreHandle,
record: &GroupOutboxRecord,
) -> Result<(EventId, Option<StoreOffset>), BaseRelayError> {
- let event = self.builder.sign_payload(record.payload())?;
- if generated_event_already_stored(store, event.id())? {
- return Ok((event.id().clone(), None));
+ let generated = GeneratedGroupStorageEvent::build(&self.builder, record.payload())?;
+ if generated_event_already_stored(store, generated.event().id())? {
+ return Ok((generated.event().id().clone(), None));
}
- let pocket_event = tangle_event_to_pocket(&event)?;
- let offset = StoreOffset::new(store.store_event(&pocket_event)?);
+ let offset = StoreOffset::new(store.store_event(generated.pocket_event())?);
self.projection
- .apply_canonical_event(&event, offset, self.limits)?;
+ .apply_canonical_event(generated.event(), offset, self.limits)?;
self.persist_group_projection(store, record.key().group_id())?;
- Ok((event.id().clone(), Some(offset)))
+ Ok((generated.event().id().clone(), Some(offset)))
}
fn persist_group_projection(
@@ -1212,24 +1238,64 @@ fn delete_target_event_id(event: &(impl GroupEventView + ?Sized)) -> Result<Even
#[cfg(test)]
mod tests {
use super::{
- GroupCheckpointStatus, GroupServiceHandle, scan_canonical_group_events,
- scan_canonical_group_events_after, validate_group_extra_tables,
+ GeneratedGroupStorageEvent, GroupCheckpointStatus, GroupServiceHandle,
+ scan_canonical_group_events, scan_canonical_group_events_after,
+ 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::{
- GroupRuntimeConfig, KIND_GROUP_METADATA, ProjectionCheckpoint, StoreOffset,
- projection_checkpoint_key,
+ GroupGeneratedEventBuilder, GroupId, GroupRuntimeConfig, KIND_GROUP_METADATA,
+ KIND_GROUP_PUT_USER, ProjectionCheckpoint, StoreOffset, projection_checkpoint_key,
};
use tangle_protocol::{Tag, UnixTimestamp};
use tangle_store_pocket::{
- PocketStoreConfig, PocketStoreHandle, PocketSyncPolicy, TANGLE_GROUP_CHECKPOINT_TABLE,
- TANGLE_GROUP_PROJECTION_TABLE,
+ PocketEvent, PocketStoreConfig, PocketStoreHandle, PocketSyncPolicy,
+ TANGLE_GROUP_CHECKPOINT_TABLE, TANGLE_GROUP_PROJECTION_TABLE,
};
use tangle_test_support::{
FixtureKey, tangle_v2_event, tangle_v2_group_create_event, tangle_v2_group_event,
};
#[test]
+ fn generated_group_storage_event_adapter_preserves_pocket_id_signature_and_tags() {
+ let builder = GroupGeneratedEventBuilder::new(
+ RelaySigner::from_secret_hex(&"7".repeat(64)).expect("key"),
+ );
+ let group_id = GroupId::new("PocketFarm").expect("group");
+ let member = FixtureKey::Member.public_key();
+ let payload = GroupGeneratedEventBuilder::join_accepted_payload(
+ &group_id,
+ &member,
+ UnixTimestamp::new(1_714_124_433),
+ );
+ let generated = GeneratedGroupStorageEvent::build(&builder, &payload).expect("generated");
+
+ assert_eq!(
+ generated.pocket_event().id().as_hex_string(),
+ generated.event().id().as_str()
+ );
+ assert_eq!(
+ generated.pocket_event().pubkey().as_hex_string(),
+ generated.event().unsigned().pubkey().as_str()
+ );
+ assert_eq!(
+ u32::from(generated.pocket_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");
+ }
+
+ #[test]
fn group_service_from_disabled_config_is_absent() {
let root = std::env::temp_dir().join(format!(
"tangle-group-service-disabled-{}",
@@ -1414,4 +1480,11 @@ mod tests {
let store = PocketStoreHandle::open(&config).expect("store");
(root, store)
}
+
+ 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("utf8"))
+ .eq(expected.iter().copied())
+ })
+ }
}