1use std::fmt::Debug;
4use std::marker::PhantomData;
5
6use p2panda_core::{Extension, Extensions, Hash, LogId, Operation, VerifyingKey};
7use p2panda_store::groups::GroupsStore;
8use p2panda_store::operations::OperationStore;
9use p2panda_store::{SqliteError, SqliteStore, Transaction};
10use p2panda_stream::ingest::{IngestError, ingest_operation};
11use p2panda_stream::orderer::CausalOrderer;
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14use tracing::{debug, trace};
15
16use crate::group;
17use crate::processor::{GroupsArgs, GroupsOperation};
18use crate::traits::{Conditions, IdentityHandle, Operation as GroupsOperationTrait, OperationId};
19
20type GroupsCrdt<C> = group::GroupCrdt<VerifyingKey, Hash, GroupsOperation<C>, C, StrongRemove<C>>;
21type GroupsCrdtError<C> =
22 group::GroupCrdtError<VerifyingKey, Hash, GroupsOperation<C>, C, StrongRemove<C>>;
23type StrongRemove<C> = group::resolver::StrongRemove<VerifyingKey, Hash, GroupsOperation<C>, C>;
24type GroupsState<C> = group::GroupCrdtState<VerifyingKey, Hash, GroupsOperation<C>, C>;
25
26impl IdentityHandle for VerifyingKey {}
27impl OperationId for Hash {}
28
29#[derive(Clone)]
31pub struct GroupsProcessor<T, E, L, C = ()> {
32 store: SqliteStore,
33 orderer: CausalOrderer<Hash, SqliteStore>,
34 _marker: PhantomData<(T, E, L, C)>,
35}
36
37impl<T, E, L, C> GroupsProcessor<T, E, L, C>
38where
39 T: Serialize + for<'de> Deserialize<'de>,
40 E: Extensions + Extension<GroupsArgs<C>> + Extension<L>,
41 L: LogId,
42 C: Conditions + Serialize + for<'a> Deserialize<'a>,
43{
44 pub fn new(store: SqliteStore) -> Self {
45 Self {
46 store: store.clone(),
47 orderer: CausalOrderer::new(store),
48 _marker: Default::default(),
49 }
50 }
51 pub async fn process<SID>(
60 &self,
61 id: &SID,
62 topic: &T,
63 operation: &Operation<E>,
64 ) -> Result<(), GroupsProcessorError<C>>
65 where
66 SID: for<'a> Deserialize<'a> + Serialize,
67 {
68 let Some(log_id): Option<L> = operation.header.extension() else {
72 return Err(GroupsProcessorError::MissingLogId);
73 };
74
75 ingest_operation(&self.store, &operation, &log_id, topic, false).await?;
78
79 let Some(args) = operation.header.extension::<GroupsArgs<C>>() else {
83 trace!(id = operation.hash.to_hex(), "ignore non-groups operation");
84 return Ok(());
85 };
86
87 let groups_operation = GroupsOperation {
88 id: operation.hash,
89 author: operation.header.verifying_key,
90 dependencies: args.dependencies,
91 group_id: args.group_id,
92 action: args.action,
93 };
94
95 debug!(id = operation.hash.to_hex(), "process groups operation");
96
97 let permit = self.store.begin().await?;
99
100 let operation_id = groups_operation.id();
104 let dependencies = groups_operation.dependencies();
105 self.orderer.process(operation_id, &dependencies).await?;
106
107 let mut y = GroupsStore::<SID, GroupsState<C>>::get_groups_state(&self.store, id)
111 .await?
112 .unwrap_or_default();
113
114 debug!(
115 group_id = groups_operation.group_id().to_hex(),
116 "current group membership: {:?}",
117 y.members(groups_operation.group_id())
118 );
119
120 while let Some(id) = self.orderer.next().await? {
123 debug!(id = id.to_hex(), "apply ready operation to groups state");
124 let groups_operation = get_groups_operation::<E, L, C>(&self.store, &id).await?;
125 y = GroupsCrdt::process(y, &groups_operation)?;
126 }
127
128 self.store.set_groups_state(id, &y).await?;
130
131 self.store.commit(permit).await?;
133
134 debug!(
135 group_id = groups_operation.group_id().to_hex(),
136 "new group membership: {:?}",
137 y.members(groups_operation.group_id())
138 );
139
140 Ok(())
141 }
142}
143
144async fn get_groups_operation<E, L, C>(
146 store: &SqliteStore,
147 id: &Hash,
148) -> Result<GroupsOperation<C>, GroupsProcessorError<C>>
149where
150 E: Extensions + Extension<GroupsArgs<C>> + Extension<L>,
151 L: LogId,
152 C: Conditions + Serialize + for<'a> Deserialize<'a>,
153{
154 let operation =
155 <SqliteStore as OperationStore<Operation<E>, Hash, L>>::get_operation_tx(store, id).await?;
156
157 let operation = match operation {
158 Some(operation) => {
159 let Some(args) = operation.header.extension::<GroupsArgs<C>>() else {
160 return Err(GroupsProcessorError::<C>::MissingOperation(*id));
161 };
162
163 GroupsOperation {
164 id: operation.hash,
165 author: operation.header.verifying_key,
166 dependencies: args.dependencies,
167 group_id: args.group_id,
168 action: args.action,
169 }
170 }
171 None => {
172 return Err(GroupsProcessorError::<C>::MissingOperation(*id));
173 }
174 };
175 Ok(operation)
176}
177
178#[allow(clippy::large_enum_variant)]
180#[derive(Debug, Error)]
181pub enum GroupsProcessorError<C>
182where
183 C: Conditions,
184{
185 #[error(transparent)]
186 Store(#[from] SqliteError),
187
188 #[error(transparent)]
189 Groups(#[from] GroupsCrdtError<C>),
190
191 #[error(transparent)]
192 Ingest(#[from] IngestError),
193
194 #[error("missing operation: {0}")]
195 MissingOperation(Hash),
196
197 #[error("operation retrieved from store missing groups arguments: {0}")]
198 MissingGroupsArgs(Hash),
199
200 #[error("missing \"log id\" operation extension")]
201 MissingLogId,
202}
203
204#[cfg(test)]
205mod tests {
206 use p2panda_core::test_utils::TestLog;
207 use p2panda_core::test_utils::setup_logging;
208 use p2panda_core::traits::Digest;
209 use p2panda_core::{Extension, Hash, Header, Operation, SigningKey, Topic, VerifyingKey};
210 use p2panda_store::groups::GroupsStore;
211 use p2panda_store::{SqliteStore, Transaction};
212 use serde::{Deserialize, Serialize};
213
214 use crate::Access;
215 use crate::group::{GroupAction, GroupCrdtState, GroupMember};
216 use crate::processor::{GroupsArgs, GroupsOperation};
217
218 type LogId = u64;
219 type GroupsState = GroupCrdtState<VerifyingKey, Hash, GroupsOperation, ()>;
220 type GroupsProcessor = crate::processor::GroupsProcessor<Topic, TestExtensions, LogId>;
221
222 const LOG_ID: u64 = 0;
223
224 #[derive(Debug, Clone, Serialize, Deserialize)]
225 struct TestExtensions {
226 log_id: LogId,
227 groups: Option<GroupsArgs>,
228 }
229
230 impl Extension<GroupsArgs> for TestExtensions {
231 fn extract(header: &Header<Self>) -> Option<GroupsArgs> {
232 header.extensions.groups.clone()
233 }
234 }
235
236 impl Extension<LogId> for TestExtensions {
237 fn extract(header: &Header<Self>) -> Option<LogId> {
238 Some(header.extensions.log_id)
239 }
240 }
241
242 impl From<GroupsArgs> for TestExtensions {
243 fn from(args: GroupsArgs) -> Self {
244 TestExtensions {
245 groups: Some(args),
246 log_id: LOG_ID,
247 }
248 }
249 }
250
251 #[tokio::test]
252 async fn ooo_operations() {
253 setup_logging();
254 let topic = Topic::random();
255
256 let alice_log = TestLog::new();
257 let alice = alice_log.author();
258 let bobby_log = TestLog::new();
259 let bobby = bobby_log.author();
260 let cathy_log = TestLog::new();
261 let cathy = cathy_log.author();
262
263 let state_id = 0;
264 let group_id = SigningKey::generate().verifying_key();
265
266 let args = GroupsArgs {
267 group_id,
268 action: GroupAction::Create {
269 initial_members: vec![
270 (GroupMember::Individual(alice), <Access>::manage()),
271 (GroupMember::Individual(bobby), <Access>::manage()),
272 ],
273 },
274 dependencies: vec![],
275 };
276 let op_00: Operation<TestExtensions> = alice_log.operation(&[], TestExtensions::from(args));
277
278 let args = GroupsArgs {
279 group_id,
280 action: GroupAction::Add {
281 member: GroupMember::Individual(cathy),
282 access: Access::manage(),
283 },
284 dependencies: vec![op_00.hash()],
285 };
286 let op_01 = bobby_log.operation(&[], TestExtensions::from(args));
287
288 let args = GroupsArgs {
289 group_id,
290 action: GroupAction::Remove {
291 member: GroupMember::Individual(alice),
292 },
293 dependencies: vec![op_01.hash()],
294 };
295 let op_02 = cathy_log.operation(&[], TestExtensions::from(args));
296
297 let store = SqliteStore::temporary().await;
298 let groups = GroupsProcessor::new(store.clone());
299 groups.process(&state_id, &topic, &op_02).await.unwrap();
300 groups.process(&state_id, &topic, &op_01).await.unwrap();
301 groups.process(&state_id, &topic, &op_00).await.unwrap();
302
303 let permit = store.begin().await.unwrap();
304 let y: GroupsState = store.get_groups_state(&state_id).await.unwrap().unwrap();
305 store.commit(permit).await.unwrap();
306
307 let members = y.members(group_id);
308 assert_eq!(members.len(), 2);
309 assert!(members.contains(&(bobby, Access::manage())));
310 assert!(members.contains(&(cathy, Access::manage())));
311 }
312
313 #[tokio::test]
314 async fn device_groups_single_context() {
315 let topic = Topic::random();
316
317 let state_id = 'S';
319
320 let alice_store = SqliteStore::temporary().await;
321 let bobby_store = SqliteStore::temporary().await;
322 let cathy_store = SqliteStore::temporary().await;
323
324 let alice_log = TestLog::new();
325 let alice = alice_log.author();
326 let bobby_log = TestLog::new();
327 let bobby = bobby_log.author();
328 let cathy_log = TestLog::new();
329 let cathy = cathy_log.author();
330
331 let alice_device_group = SigningKey::generate().verifying_key();
332 let bobby_device_group = SigningKey::generate().verifying_key();
333 let cathy_device_group = SigningKey::generate().verifying_key();
334 let ab_chat = SigningKey::generate().verifying_key();
335 let bc_chat = SigningKey::generate().verifying_key();
336
337 let alice_groups = GroupsProcessor::new(alice_store.clone());
338 let bobby_groups = GroupsProcessor::new(bobby_store.clone());
339 let cathy_groups = GroupsProcessor::new(cathy_store.clone());
340
341 let args = GroupsArgs {
344 group_id: alice_device_group,
345 action: GroupAction::Create {
346 initial_members: vec![(GroupMember::Individual(alice), Access::manage())],
347 },
348 dependencies: vec![],
349 };
350 let create_alice_device_00: Operation<TestExtensions> =
351 alice_log.operation(&[], TestExtensions::from(args));
352
353 alice_groups
354 .process(&state_id, &topic, &create_alice_device_00)
355 .await
356 .unwrap();
357
358 let args = GroupsArgs {
359 group_id: bobby_device_group,
360 action: GroupAction::Create {
361 initial_members: vec![(GroupMember::Individual(bobby), Access::manage())],
362 },
363 dependencies: vec![],
364 };
365 let create_bobby_device_01: Operation<TestExtensions> =
366 bobby_log.operation(&[], TestExtensions::from(args));
367
368 bobby_groups
369 .process(&state_id, &topic, &create_bobby_device_01)
370 .await
371 .unwrap();
372
373 let args = GroupsArgs {
374 group_id: cathy_device_group,
375 action: GroupAction::Create {
376 initial_members: vec![(GroupMember::Individual(cathy), Access::manage())],
377 },
378 dependencies: vec![],
379 };
380 let create_cathy_device_02: Operation<TestExtensions> =
381 cathy_log.operation(&[], TestExtensions::from(args));
382
383 cathy_groups
384 .process(&state_id, &topic, &create_cathy_device_02)
385 .await
386 .unwrap();
387
388 alice_groups
392 .process(&state_id, &topic, &create_bobby_device_01)
393 .await
394 .unwrap();
395
396 let permit = alice_store.begin().await.unwrap();
398 let y: GroupsState = alice_store
399 .get_groups_state(&state_id)
400 .await
401 .unwrap()
402 .unwrap();
403 alice_store.commit(permit).await.unwrap();
404
405 let args = GroupsArgs {
406 group_id: ab_chat,
407 action: GroupAction::Create {
408 initial_members: vec![
409 (GroupMember::Group(alice_device_group), Access::write()),
410 (GroupMember::Group(bobby_device_group), Access::write()),
411 ],
412 },
413 dependencies: y.heads_filtered(&[alice_device_group, bobby_device_group]),
414 };
415 let create_alice_bobby_chat_03: Operation<TestExtensions> =
416 alice_log.operation(&[], TestExtensions::from(args));
417
418 alice_groups
419 .process(&state_id, &topic, &create_alice_bobby_chat_03)
420 .await
421 .unwrap();
422
423 for op in [create_alice_device_00.clone(), create_alice_bobby_chat_03] {
425 bobby_groups.process(&state_id, &topic, &op).await.unwrap();
426 }
427
428 for store in [alice_store.clone(), bobby_store.clone()] {
430 let permit = store.begin().await.unwrap();
431 let y: GroupsState = store.get_groups_state(&state_id).await.unwrap().unwrap();
432 store.commit(permit).await.unwrap();
433 let mut members = y.members(ab_chat);
434 members.sort();
435
436 assert_eq!(members.len(), 2);
437 assert!(members.contains(&(alice, Access::write())));
438 assert!(members.contains(&(bobby, Access::write())));
439 }
440
441 cathy_groups
445 .process(&state_id, &topic, &create_bobby_device_01)
446 .await
447 .unwrap();
448
449 let permit = cathy_store.begin().await.unwrap();
451 let y: GroupsState = cathy_store
452 .get_groups_state(&state_id)
453 .await
454 .unwrap()
455 .unwrap();
456 cathy_store.commit(permit).await.unwrap();
457 let args = GroupsArgs {
458 group_id: bc_chat,
459 action: GroupAction::Create {
460 initial_members: vec![
461 (GroupMember::Group(bobby_device_group), Access::write()),
462 (GroupMember::Group(cathy_device_group), Access::write()),
463 ],
464 },
465 dependencies: y.heads_filtered(&[bobby_device_group, cathy_device_group]),
466 };
467 let create_bobby_cathy_chat_04: Operation<TestExtensions> =
468 cathy_log.operation(&[], TestExtensions::from(args));
469
470 cathy_groups
471 .process(&state_id, &topic, &create_bobby_cathy_chat_04)
472 .await
473 .unwrap();
474
475 for op in [create_cathy_device_02.clone(), create_bobby_cathy_chat_04] {
477 bobby_groups.process(&state_id, &topic, &op).await.unwrap();
478 }
479
480 for store in [cathy_store, bobby_store] {
482 let permit = store.begin().await.unwrap();
483 let y: GroupsState = store.get_groups_state(&state_id).await.unwrap().unwrap();
484 store.commit(permit).await.unwrap();
485 let mut members = y.members(bc_chat);
486 members.sort();
487
488 assert_eq!(members.len(), 2);
489 assert!(members.contains(&(bobby, Access::write())));
490 assert!(members.contains(&(cathy, Access::write())));
491 }
492 }
493}