1use std::borrow::Borrow;
4use std::cell::RefCell;
5use std::collections::VecDeque;
6use std::fmt::Debug;
7use std::marker::PhantomData;
8
9use p2panda_auth::traits::{Conditions, Operation as GroupsOperationTrait};
10use p2panda_auth::{GroupsExtensionArgs, group};
11use p2panda_core::{Extension, Extensions, Hash, LogId, VerifyingKey};
12use p2panda_store::groups::GroupsStore;
13use p2panda_store::{SqliteError, SqliteStore, Transaction};
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16use tokio::sync::Notify;
17use tracing::{debug, trace};
18
19use crate::Processor;
20use crate::groups::{GroupsArgs, GroupsOperation};
21
22type GroupsCrdt<C> = group::GroupCrdt<VerifyingKey, Hash, GroupsOperation<C>, C, StrongRemove<C>>;
23
24type GroupsCrdtError<C> =
25 group::GroupCrdtError<VerifyingKey, Hash, GroupsOperation<C>, C, StrongRemove<C>>;
26
27type StrongRemove<C> = group::resolver::StrongRemove<VerifyingKey, Hash, GroupsOperation<C>, C>;
28
29#[derive(Clone)]
30pub enum GroupsResult {
31 Processed,
32 Noop,
33}
34
35impl GroupsResult {
36 pub fn was_processed(self) -> bool {
37 match self {
38 Self::Processed => true,
39 Self::Noop => false,
40 }
41 }
42}
43
44pub struct Groups<T, E, L, C = ()> {
46 store: SqliteStore,
47 notify: Notify,
48 queue: RefCell<VecDeque<(T, GroupsResult)>>,
49 _marker: PhantomData<(E, L, C)>,
50}
51
52impl<T, E, L, C> Groups<T, E, L, C>
53where
54 E: Extensions + Extension<GroupsExtensionArgs<C>> + Extension<L>,
55 L: LogId,
56 C: Conditions + Serialize + for<'a> Deserialize<'a>,
57{
58 pub fn new(store: SqliteStore) -> Self {
59 Self {
60 store: store.clone(),
61 notify: Notify::new(),
62 queue: RefCell::new(VecDeque::new()),
63 _marker: Default::default(),
64 }
65 }
66}
67
68impl<T, E, L, C> Processor<T> for Groups<T, E, L, C>
69where
70 T: Borrow<GroupsArgs<E>>,
71 E: Extensions + Extension<GroupsExtensionArgs<C>> + Extension<L>,
72 L: LogId,
73 C: Conditions + Serialize + for<'a> Deserialize<'a>,
74{
75 type Output = (T, GroupsResult);
76
77 type Error = (T, GroupsError<C>);
78
79 async fn process(&self, input: T) -> Result<(), Self::Error> {
80 let input_args: &GroupsArgs<E> = input.borrow();
81
82 let result = if let GroupsArgs::Process {
83 state_id,
84 operation,
85 } = input_args &&
86 let Some(args) = operation.header.extension::<GroupsExtensionArgs<C>>()
91 {
92 let groups_operation = GroupsOperation {
94 id: operation.hash,
95 author: operation.header.verifying_key,
96 dependencies: args.dependencies,
97 group_id: args.group_id,
98 action: args.action,
99 };
100
101 let permit = match self.store.begin().await {
103 Ok(permit) => permit,
104 Err(err) => return Err((input, err.into())),
105 };
106
107 let mut y = match GroupsStore::<GroupsOperation<C>, C>::get_groups_state_tx(
109 &self.store,
110 *state_id,
111 )
112 .await
113 {
114 Err(err) => return Err((input, err.into())),
115 Ok(Some(y)) => y,
116 Ok(None) => Default::default(),
117 };
118
119 debug!(
120 group_id = groups_operation.group_id().to_hex(),
121 "current group membership: {:?}",
122 y.members(groups_operation.group_id())
123 );
124
125 debug!(
126 id = groups_operation.id.to_hex(),
127 "apply operation to group state"
128 );
129 y = match GroupsCrdt::process(y, &groups_operation) {
130 Ok(y) => y,
131 Err(err) => return Err((input, err.into())),
132 };
133
134 if let Err(err) = self.store.set_groups_state_tx(*state_id, &y).await {
136 return Err((input, err.into()));
137 }
138
139 if let Err(err) = self.store.commit(permit).await {
141 return Err((input, err.into()));
142 }
143
144 debug!(
145 group_id = groups_operation.group_id().to_hex(),
146 "new group membership: {:?}",
147 y.members(groups_operation.group_id())
148 );
149
150 (input, GroupsResult::Processed)
151 } else {
152 trace!("ignore non-groups operation");
153 (input, GroupsResult::Noop)
154 };
155
156 self.queue.borrow_mut().push_back(result);
157 self.notify.notify_one(); Ok(())
160 }
161
162 async fn next(&self) -> Result<Self::Output, Self::Error> {
163 loop {
164 if let Some(item) = self.queue.borrow_mut().pop_front() {
165 return Ok(item);
166 }
167
168 self.notify.notified().await;
170 }
171 }
172}
173
174#[allow(clippy::large_enum_variant)]
176#[derive(Debug, Error)]
177pub enum GroupsError<C>
178where
179 C: Conditions,
180{
181 #[error(transparent)]
182 Store(#[from] SqliteError),
183
184 #[error(transparent)]
185 Groups(#[from] GroupsCrdtError<C>),
186
187 #[error("missing operation: {0}")]
188 MissingOperation(Hash),
189
190 #[error("operation retrieved from store missing groups arguments: {0}")]
191 MissingGroupsArgs(Hash),
192
193 #[error("missing \"log id\" operation extension")]
194 MissingLogId,
195}
196
197#[cfg(test)]
198mod tests {
199 use std::borrow::Borrow;
200
201 use p2panda_auth::group::{GroupAction, GroupCrdtState, GroupMember};
202 use p2panda_auth::{Access, GroupsExtensionArgs};
203 use p2panda_core::test_utils::{TestLog, setup_logging};
204 use p2panda_core::traits::Digest;
205 use p2panda_core::{Extension, Hash, Header, Operation, SigningKey, Topic, VerifyingKey};
206 use p2panda_store::groups::GroupsStore;
207 use p2panda_store::{SqliteStore, Transaction};
208 use serde::{Deserialize, Serialize};
209
210 use crate::Processor;
211 use crate::groups::GroupsOperation;
212 use crate::ingest::{Ingest, IngestArgs};
213 use crate::orderer::{Orderer, Ordering};
214
215 use super::GroupsArgs;
216
217 type LogId = usize;
218 type GroupsState = GroupCrdtState<VerifyingKey, Hash, GroupsOperation, ()>;
219 type Groups = crate::groups::Groups<GroupsArgs<TestExtensions>, TestExtensions, LogId>;
220
221 const LOG_ID: usize = 0;
222
223 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
224 struct TestExtensions {
225 log_id: LogId,
226 dependencies: Vec<Hash>,
227 groups: Option<GroupsExtensionArgs>,
228 }
229
230 impl Extension<LogId> for TestExtensions {
231 fn extract(header: &Header<Self>) -> Option<LogId> {
232 Some(header.extensions.log_id)
233 }
234 }
235
236 impl Extension<GroupsExtensionArgs> for TestExtensions {
237 fn extract(header: &Header<Self>) -> Option<GroupsExtensionArgs> {
238 header.extensions.groups.clone()
239 }
240 }
241
242 impl Ordering<Hash> for Operation<TestExtensions> {
243 fn dependencies(&self) -> &[Hash] {
244 &self.header.extensions.dependencies
245 }
246 }
247
248 impl From<GroupsExtensionArgs> for TestExtensions {
249 fn from(args: GroupsExtensionArgs) -> Self {
250 TestExtensions {
251 log_id: LOG_ID,
252 dependencies: args.dependencies.clone(),
253 groups: Some(args),
254 }
255 }
256 }
257
258 #[derive(Clone, Debug, PartialEq, Eq)]
259 struct IngestEvent<E> {
260 pub operation: Operation<E>,
261 pub args: IngestArgs<usize, Topic>,
262 }
263
264 impl<E> Borrow<IngestArgs<usize, Topic>> for IngestEvent<E> {
265 fn borrow(&self) -> &IngestArgs<usize, Topic> {
266 &self.args
267 }
268 }
269
270 impl<E> Borrow<Operation<E>> for IngestEvent<E> {
271 fn borrow(&self) -> &Operation<E> {
272 &self.operation
273 }
274 }
275
276 #[tokio::test]
277 async fn basic_processing() {
278 setup_logging();
279
280 let topic = Topic::random();
281
282 let state_id = Hash::digest(b"default");
283 let group_id = SigningKey::generate().verifying_key();
284
285 let alice_log = TestLog::new();
286 let bobby_log = TestLog::new();
287
288 let alice = alice_log.author();
289 let bobby = bobby_log.author();
290
291 let args = GroupsExtensionArgs {
292 group_id,
293 action: GroupAction::Create {
294 initial_members: vec![
295 (GroupMember::Individual(alice), <Access>::manage()),
296 (GroupMember::Individual(bobby), <Access>::manage()),
297 ],
298 },
299 dependencies: vec![],
300 };
301 let op_00: Operation<TestExtensions> = alice_log.operation(&[], TestExtensions::from(args));
302
303 let store = SqliteStore::temporary().await;
304
305 let ingest = Ingest::new(store.clone());
307 ingest
308 .process(IngestEvent {
309 operation: op_00.clone(),
310 args: IngestArgs {
311 log_id: LOG_ID.into(),
312 topic,
313 prune_flag: false,
314 },
315 })
316 .await
317 .unwrap();
318
319 let groups = Groups::new(store.clone());
320 groups
321 .process(GroupsArgs::Process {
322 state_id,
323 operation: op_00,
324 })
325 .await
326 .unwrap();
327 let (_processed_op, result) = groups.next().await.unwrap();
328 assert!(result.was_processed());
329
330 let permit = store.begin().await.unwrap();
331 let y: GroupsState = store.get_groups_state_tx(state_id).await.unwrap().unwrap();
332 store.commit(permit).await.unwrap();
333
334 let members = y.members(group_id);
335 assert_eq!(members.len(), 2);
336 assert!(members.contains(&(alice, Access::manage())));
337 assert!(members.contains(&(bobby, Access::manage())));
338 }
339
340 #[tokio::test]
341 async fn ooo_operations() {
342 setup_logging();
343 let topic = Topic::random();
344
345 let state_id = Hash::digest(b"default");
346 let group_id = SigningKey::generate().verifying_key();
347
348 let alice_log = TestLog::new();
349 let bobby_log = TestLog::new();
350 let cathy_log = TestLog::new();
351
352 let alice = alice_log.author();
353 let bobby = bobby_log.author();
354 let cathy = cathy_log.author();
355
356 let args = GroupsExtensionArgs {
358 group_id,
359 action: GroupAction::Create {
360 initial_members: vec![
361 (GroupMember::Individual(alice), <Access>::manage()),
362 (GroupMember::Individual(bobby), <Access>::manage()),
363 ],
364 },
365 dependencies: vec![],
366 };
367 let op_00: Operation<TestExtensions> = alice_log.operation(&[], TestExtensions::from(args));
368
369 let args = GroupsExtensionArgs {
371 group_id,
372 action: GroupAction::Add {
373 member: GroupMember::Individual(cathy),
374 access: Access::manage(),
375 },
376 dependencies: vec![op_00.hash()],
377 };
378 let op_01: Operation<TestExtensions> = bobby_log.operation(&[], TestExtensions::from(args));
379
380 let args = GroupsExtensionArgs {
382 group_id,
383 action: GroupAction::Remove {
384 member: GroupMember::Individual(alice),
385 },
386 dependencies: vec![op_01.hash()],
387 };
388 let op_02: Operation<TestExtensions> = cathy_log.operation(&[], TestExtensions::from(args));
389
390 let store = SqliteStore::temporary().await;
391 let ingest = Ingest::new(store.clone());
392 let orderer = Orderer::new(store.clone());
393 let groups = Groups::new(store.clone());
394
395 ingest
396 .process(IngestEvent {
397 operation: op_02.clone(),
398 args: IngestArgs {
399 log_id: LOG_ID.into(),
400 topic,
401 prune_flag: false,
402 },
403 })
404 .await
405 .unwrap();
406
407 ingest
408 .process(IngestEvent {
409 operation: op_01.clone(),
410 args: IngestArgs {
411 log_id: LOG_ID.into(),
412 topic,
413 prune_flag: false,
414 },
415 })
416 .await
417 .unwrap();
418
419 ingest
420 .process(IngestEvent {
421 operation: op_00.clone(),
422 args: IngestArgs {
423 log_id: LOG_ID.into(),
424 topic,
425 prune_flag: false,
426 },
427 })
428 .await
429 .unwrap();
430
431 orderer.process(op_02).await.unwrap();
433 orderer.process(op_01).await.unwrap();
434 orderer.process(op_00).await.unwrap();
435
436 for _op in 0..3 {
438 let next_op = orderer.next().await.unwrap();
439 groups
440 .process(GroupsArgs::Process {
441 state_id,
442 operation: next_op,
443 })
444 .await
445 .unwrap()
446 }
447
448 let permit = store.begin().await.unwrap();
449 let y: GroupsState = store.get_groups_state_tx(state_id).await.unwrap().unwrap();
450 store.commit(permit).await.unwrap();
451
452 let members = y.members(group_id);
453 assert_eq!(members.len(), 2);
454 assert!(members.contains(&(bobby, Access::manage())));
455 assert!(members.contains(&(cathy, Access::manage())));
456 assert!(!members.contains(&(alice, Access::manage())));
457 }
458
459 #[tokio::test]
460 async fn device_groups_single_context() {
461 let topic = Topic::random();
462
463 let state_id = Hash::digest(b"default");
465
466 let alice_log = TestLog::new();
467 let bobby_log = TestLog::new();
468 let cathy_log = TestLog::new();
469
470 let alice = alice_log.author();
471 let bobby = bobby_log.author();
472 let cathy = cathy_log.author();
473
474 let alice_store = SqliteStore::temporary().await;
475 let bobby_store = SqliteStore::temporary().await;
476 let cathy_store = SqliteStore::temporary().await;
477
478 let alice_device_group = SigningKey::generate().verifying_key();
479 let bobby_device_group = SigningKey::generate().verifying_key();
480 let cathy_device_group = SigningKey::generate().verifying_key();
481
482 let ab_chat = SigningKey::generate().verifying_key();
483 let bc_chat = SigningKey::generate().verifying_key();
484
485 let alice_groups = Groups::new(alice_store.clone());
486 let bobby_groups = Groups::new(bobby_store.clone());
487 let cathy_groups = Groups::new(cathy_store.clone());
488
489 let args = GroupsExtensionArgs {
492 group_id: alice_device_group,
493 action: GroupAction::Create {
494 initial_members: vec![(GroupMember::Individual(alice), Access::manage())],
495 },
496 dependencies: vec![],
497 };
498 let create_alice_device_00: Operation<TestExtensions> =
499 alice_log.operation(&[], TestExtensions::from(args));
500
501 let alice_ingest = Ingest::new(alice_store.clone());
502 alice_ingest
503 .process(IngestEvent {
504 operation: create_alice_device_00.clone(),
505 args: IngestArgs {
506 log_id: LOG_ID.into(),
507 topic,
508 prune_flag: false,
509 },
510 })
511 .await
512 .unwrap();
513
514 alice_groups
515 .process(GroupsArgs::Process {
516 state_id,
517 operation: create_alice_device_00.clone(),
518 })
519 .await
520 .unwrap();
521
522 let args = GroupsExtensionArgs {
523 group_id: bobby_device_group,
524 action: GroupAction::Create {
525 initial_members: vec![(GroupMember::Individual(bobby), Access::manage())],
526 },
527 dependencies: vec![],
528 };
529 let create_bobby_device_01: Operation<TestExtensions> =
530 bobby_log.operation(&[], TestExtensions::from(args));
531
532 let bobby_ingest = Ingest::new(bobby_store.clone());
533 bobby_ingest
534 .process(IngestEvent {
535 operation: create_bobby_device_01.clone(),
536 args: IngestArgs {
537 log_id: LOG_ID.into(),
538 topic,
539 prune_flag: false,
540 },
541 })
542 .await
543 .unwrap();
544
545 bobby_groups
546 .process(GroupsArgs::Process {
547 state_id,
548 operation: create_bobby_device_01.clone(),
549 })
550 .await
551 .unwrap();
552
553 let args = GroupsExtensionArgs {
554 group_id: cathy_device_group,
555 action: GroupAction::Create {
556 initial_members: vec![(GroupMember::Individual(cathy), Access::manage())],
557 },
558 dependencies: vec![],
559 };
560 let create_cathy_device_02: Operation<TestExtensions> =
561 cathy_log.operation(&[], TestExtensions::from(args));
562
563 let cathy_ingest = Ingest::new(cathy_store.clone());
564 cathy_ingest
565 .process(IngestEvent {
566 operation: create_cathy_device_02.clone(),
567 args: IngestArgs {
568 log_id: LOG_ID.into(),
569 topic,
570 prune_flag: false,
571 },
572 })
573 .await
574 .unwrap();
575
576 cathy_groups
577 .process(GroupsArgs::Process {
578 state_id,
579 operation: create_cathy_device_02.clone(),
580 })
581 .await
582 .unwrap();
583
584 alice_ingest
588 .process(IngestEvent {
589 operation: create_bobby_device_01.clone(),
590 args: IngestArgs {
591 log_id: LOG_ID.into(),
592 topic,
593 prune_flag: false,
594 },
595 })
596 .await
597 .unwrap();
598
599 alice_groups
600 .process(GroupsArgs::Process {
601 state_id,
602 operation: create_bobby_device_01.clone(),
603 })
604 .await
605 .unwrap();
606
607 let permit = alice_store.begin().await.unwrap();
609 let y: GroupsState = alice_store
610 .get_groups_state_tx(state_id)
611 .await
612 .unwrap()
613 .unwrap();
614 alice_store.commit(permit).await.unwrap();
615
616 let args = GroupsExtensionArgs {
617 group_id: ab_chat,
618 action: GroupAction::Create {
619 initial_members: vec![
620 (GroupMember::Group(alice_device_group), Access::write()),
621 (GroupMember::Group(bobby_device_group), Access::write()),
622 ],
623 },
624 dependencies: y.heads_filtered(&[alice_device_group, bobby_device_group]),
625 };
626 let create_alice_bobby_chat_03: Operation<TestExtensions> =
627 alice_log.operation(&[], TestExtensions::from(args));
628
629 alice_ingest
630 .process(IngestEvent {
631 operation: create_alice_bobby_chat_03.clone(),
632 args: IngestArgs {
633 log_id: LOG_ID.into(),
634 topic,
635 prune_flag: false,
636 },
637 })
638 .await
639 .unwrap();
640
641 alice_groups
642 .process(GroupsArgs::Process {
643 state_id,
644 operation: create_alice_bobby_chat_03.clone(),
645 })
646 .await
647 .unwrap();
648
649 for op in [create_alice_device_00.clone(), create_alice_bobby_chat_03] {
651 bobby_ingest
652 .process(IngestEvent {
653 operation: op.clone(),
654 args: IngestArgs {
655 log_id: LOG_ID.into(),
656 topic,
657 prune_flag: false,
658 },
659 })
660 .await
661 .unwrap();
662 bobby_groups
663 .process(GroupsArgs::Process {
664 state_id,
665 operation: op,
666 })
667 .await
668 .unwrap();
669 }
670
671 for store in [alice_store.clone(), bobby_store.clone()] {
673 let permit = store.begin().await.unwrap();
674 let y: GroupsState = store.get_groups_state_tx(state_id).await.unwrap().unwrap();
675 store.commit(permit).await.unwrap();
676 let mut members = y.members(ab_chat);
677 members.sort();
678
679 assert_eq!(members.len(), 2);
680 assert!(members.contains(&(alice, Access::write())));
681 assert!(members.contains(&(bobby, Access::write())));
682 }
683
684 cathy_ingest
688 .process(IngestEvent {
689 operation: create_bobby_device_01.clone(),
690 args: IngestArgs {
691 log_id: LOG_ID.into(),
692 topic,
693 prune_flag: false,
694 },
695 })
696 .await
697 .unwrap();
698
699 cathy_groups
700 .process(GroupsArgs::Process {
701 state_id,
702 operation: create_bobby_device_01,
703 })
704 .await
705 .unwrap();
706
707 let permit = cathy_store.begin().await.unwrap();
709 let y: GroupsState = cathy_store
710 .get_groups_state_tx(state_id)
711 .await
712 .unwrap()
713 .unwrap();
714 cathy_store.commit(permit).await.unwrap();
715 let args = GroupsExtensionArgs {
716 group_id: bc_chat,
717 action: GroupAction::Create {
718 initial_members: vec![
719 (GroupMember::Group(bobby_device_group), Access::write()),
720 (GroupMember::Group(cathy_device_group), Access::write()),
721 ],
722 },
723 dependencies: y.heads_filtered(&[bobby_device_group, cathy_device_group]),
724 };
725 let create_bobby_cathy_chat_04: Operation<TestExtensions> =
726 cathy_log.operation(&[], TestExtensions::from(args));
727
728 cathy_ingest
729 .process(IngestEvent {
730 operation: create_bobby_cathy_chat_04.clone(),
731 args: IngestArgs {
732 log_id: LOG_ID.into(),
733 topic,
734 prune_flag: false,
735 },
736 })
737 .await
738 .unwrap();
739
740 cathy_groups
741 .process(GroupsArgs::Process {
742 state_id,
743 operation: create_bobby_cathy_chat_04.clone(),
744 })
745 .await
746 .unwrap();
747
748 for op in [create_cathy_device_02.clone(), create_bobby_cathy_chat_04] {
750 bobby_ingest
751 .process(IngestEvent {
752 operation: op.clone(),
753 args: IngestArgs {
754 log_id: LOG_ID.into(),
755 topic,
756 prune_flag: false,
757 },
758 })
759 .await
760 .unwrap();
761
762 bobby_groups
763 .process(GroupsArgs::Process {
764 state_id,
765 operation: op,
766 })
767 .await
768 .unwrap();
769 }
770
771 for store in [cathy_store, bobby_store] {
773 let permit = store.begin().await.unwrap();
774 let y: GroupsState = store.get_groups_state_tx(state_id).await.unwrap().unwrap();
775 store.commit(permit).await.unwrap();
776 let mut members = y.members(bc_chat);
777 members.sort();
778
779 assert_eq!(members.len(), 2);
780 assert!(members.contains(&(bobby, Access::write())));
781 assert!(members.contains(&(cathy, Access::write())));
782 }
783 }
784}