Skip to main content

p2panda_stream/groups/
processor.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use 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
44/// Processor for groups operations.
45pub 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            // Extract GroupArgs from the extension headers of an Operation<E>.
87            //
88            // If this returns None then the groups extension was not present and we consider this
89            // a non-groups operation which does not require processing.
90            let Some(args) = operation.header.extension::<GroupsExtensionArgs<C>>()
91        {
92            // Construct a GroupsOperation from an Operation<E>.
93            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            // Start a transaction for all following database actions.
102            let permit = match self.store.begin().await {
103                Ok(permit) => permit,
104                Err(err) => return Err((input, err.into())),
105            };
106
107            // Retrieve the current groups state from the store.
108            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            // Set the groups state after processing is finished.
135            if let Err(err) = self.store.set_groups_state_tx(*state_id, &y).await {
136                return Err((input, err.into()));
137            }
138
139            // Commit the open transaction.
140            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(); // Wake up any pending recv.
158
159        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            // Wait for notification that an item was added.
169            self.notify.notified().await;
170        }
171    }
172}
173
174/// Error types which can occur in the groups processor.
175#[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        // The operation needs to be ingested before it can be processed by the groups processor.
306        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        // Alice operation.
357        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        // Bobby operation.
370        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        // Cathy operation.
381        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        // Operations are processed by the orderer in reverse order.
432        orderer.process(op_02).await.unwrap();
433        orderer.process(op_01).await.unwrap();
434        orderer.process(op_00).await.unwrap();
435
436        // Each operation freed from the orderer is processed by the groups processor.
437        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        // All operations are processed on the same groups state context.
464        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        // All members create their own device groups and process them on their own stores.
490
491        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 creates chat with Bobby.
585        //
586        // First they process "create device group" operation from Bobby.
587        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        // Then they create the chat group.
608        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        // Bobby processes alice's "create device group" and "create ab chat".
650        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        // Both Alice and Bobby have the correct groups state.
672        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 now creates a chat with Bobby.
685        //
686        // First they process "create device group" for bobby.
687        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        // Then they create the chat group.
708        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        // Bobby processes cathy's "create device group" and "create bc chat".
749        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        // Both Cathy and Bobby have the correct groups state.
772        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}