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::group;
10use p2panda_auth::traits::{Conditions, Operation as GroupsOperationTrait};
11use p2panda_core::{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;
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,
55    L: LogId,
56    C: Conditions + Serialize + for<'a> Deserialize<'a>,
57{
58    pub fn new(store: SqliteStore) -> Self {
59        Self {
60            store,
61            notify: Notify::new(),
62            queue: RefCell::new(VecDeque::new()),
63            _marker: PhantomData,
64        }
65    }
66}
67
68impl<T, E, L, C> Processor<T> for Groups<T, E, L, C>
69where
70    T: Borrow<GroupsArgs<C>>,
71    E: Extensions,
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<C> = input.borrow();
81
82        // Extract GroupArgs from the extension headers of an Operation<E>.
83        //
84        // If this returns None then the groups extension was not present and we consider this a
85        // non-groups operation which does not require processing.
86        let result = if let GroupsArgs::Process {
87            state_id,
88            operation,
89        } = input_args
90        {
91            let permit = match self.store.begin().await {
92                Ok(permit) => permit,
93                Err(err) => return Err((input, err.into())),
94            };
95
96            let mut y = match GroupsStore::<GroupsOperation<C>, C>::get_groups_state_tx(
97                &self.store,
98                *state_id,
99            )
100            .await
101            {
102                Err(err) => return Err((input, err.into())),
103                Ok(Some(y)) => y,
104                Ok(None) => Default::default(),
105            };
106
107            debug!(
108                group_id = %operation.group_id(),
109                "current group membership: {:?}",
110                y.members(operation.group_id())
111            );
112
113            debug!(id = %operation.id, "apply operation to group state");
114
115            y = match GroupsCrdt::process(y, operation) {
116                Ok(y) => y,
117                Err(err) => return Err((input, err.into())),
118            };
119
120            if let Err(err) = self.store.set_groups_state_tx(*state_id, &y).await {
121                return Err((input, err.into()));
122            }
123
124            if let Err(err) = self.store.commit(permit).await {
125                return Err((input, err.into()));
126            }
127
128            debug!(
129                group_id = %operation.group_id(),
130                "new group membership: {:?}",
131                y.members(operation.group_id())
132            );
133
134            (input, GroupsResult::Processed)
135        } else {
136            (input, GroupsResult::Noop)
137        };
138
139        self.queue.borrow_mut().push_back(result);
140        self.notify.notify_one(); // Wake up any pending recv.
141
142        Ok(())
143    }
144
145    async fn next(&self) -> Result<Self::Output, Self::Error> {
146        loop {
147            if let Some(item) = self.queue.borrow_mut().pop_front() {
148                return Ok(item);
149            }
150
151            // Wait for notification that an item was added.
152            self.notify.notified().await;
153        }
154    }
155}
156
157/// Error types which can occur in the groups processor.
158#[allow(clippy::large_enum_variant)]
159#[derive(Debug, Error)]
160pub enum GroupsError<C>
161where
162    C: Conditions,
163{
164    #[error(transparent)]
165    Store(#[from] SqliteError),
166
167    #[error(transparent)]
168    Groups(#[from] GroupsCrdtError<C>),
169}
170
171#[cfg(test)]
172mod tests {
173    use std::borrow::Borrow;
174
175    use p2panda_auth::group::{GroupAction, GroupCrdtState, GroupMember};
176    use p2panda_auth::{Access, GroupsExtensionArgs};
177    use p2panda_core::test_utils::{TestLog, setup_logging};
178    use p2panda_core::traits::{Digest, Provenance};
179    use p2panda_core::{Hash, Operation, SigningKey, Topic, VerifyingKey};
180    use p2panda_store::groups::GroupsStore;
181    use p2panda_store::{SqliteStore, Transaction, tx_unwrap};
182    use serde::{Deserialize, Serialize};
183
184    use crate::Processor;
185    use crate::groups::{GroupsArgs, GroupsOperation};
186    use crate::ingest::{Ingest, IngestArgs};
187    use crate::orderer::Ordering;
188
189    type LogId = usize;
190
191    type GroupsState = GroupCrdtState<VerifyingKey, Hash, GroupsOperation, ()>;
192
193    type Groups = crate::groups::Groups<Event, TestExtensions, LogId, ()>;
194
195    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196    struct TestExtensions {
197        log_id: LogId,
198        dependencies: Vec<Hash>,
199        groups: Option<GroupsExtensionArgs>,
200    }
201
202    impl From<GroupsExtensionArgs> for TestExtensions {
203        fn from(args: GroupsExtensionArgs) -> Self {
204            TestExtensions {
205                log_id: 0,
206                dependencies: args.dependencies.clone(),
207                groups: Some(args),
208            }
209        }
210    }
211
212    #[derive(Clone, Debug, PartialEq, Eq)]
213    struct Event {
214        pub operation: Operation<TestExtensions>,
215        pub ingest_args: IngestArgs<LogId, Topic>,
216        pub groups_args: GroupsArgs<()>,
217    }
218
219    impl From<Operation<TestExtensions>> for Event {
220        fn from(operation: Operation<TestExtensions>) -> Self {
221            Self {
222                ingest_args: IngestArgs {
223                    log_id: operation.header.extensions.log_id,
224                    topic: Hash::digest("test").into(),
225                    prune_flag: false,
226                },
227                groups_args: match operation.header.extensions.groups {
228                    Some(ref groups) => GroupsArgs::Process {
229                        // All operations are processed on the same groups state context.
230                        state_id: Hash::digest("default"),
231                        operation: GroupsOperation {
232                            id: operation.hash(),
233                            author: operation.author(),
234                            dependencies: operation.header.extensions.dependencies.clone(),
235                            group_id: groups.group_id,
236                            action: groups.action.clone(),
237                        },
238                    },
239                    None => GroupsArgs::Ignore,
240                },
241                operation,
242            }
243        }
244    }
245
246    // Ingest
247
248    impl Borrow<IngestArgs<LogId, Topic>> for Event {
249        fn borrow(&self) -> &IngestArgs<LogId, Topic> {
250            &self.ingest_args
251        }
252    }
253
254    impl Borrow<Operation<TestExtensions>> for Event {
255        fn borrow(&self) -> &Operation<TestExtensions> {
256            &self.operation
257        }
258    }
259
260    // Orderer
261
262    impl Ordering<Hash> for Operation<TestExtensions> {
263        fn dependencies(&self) -> &[Hash] {
264            &self.header.extensions.dependencies
265        }
266    }
267
268    // Groups
269
270    impl Borrow<GroupsArgs<()>> for Event {
271        fn borrow(&self) -> &GroupsArgs<()> {
272            &self.groups_args
273        }
274    }
275
276    #[tokio::test]
277    async fn basic_processing() {
278        setup_logging();
279
280        let store = SqliteStore::temporary().await;
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
302        let operation: Operation<TestExtensions> =
303            alice_log.operation(&[], TestExtensions::from(args.clone()));
304
305        let event = Event::from(operation);
306
307        // Operation needs to be ingested before it can be processed by the groups processor.
308        let ingest = Ingest::new(store.clone());
309        ingest.process(event.clone()).await.unwrap();
310
311        let groups = Groups::new(store.clone());
312        groups.process(event).await.unwrap();
313
314        let (_processed_op, result) = groups.next().await.unwrap();
315        assert!(result.was_processed());
316
317        let permit = store.begin().await.unwrap();
318        let y: GroupsState = store.get_groups_state_tx(state_id).await.unwrap().unwrap();
319        store.commit(permit).await.unwrap();
320
321        let members = y.members(group_id);
322        assert_eq!(members.len(), 2);
323        assert!(members.contains(&(alice, Access::manage())));
324        assert!(members.contains(&(bobby, Access::manage())));
325    }
326
327    #[tokio::test]
328    async fn device_groups_single_context() {
329        let alice_log = TestLog::new();
330        let bobby_log = TestLog::new();
331        let cathy_log = TestLog::new();
332
333        let alice = alice_log.author();
334        let bobby = bobby_log.author();
335        let cathy = cathy_log.author();
336
337        let alice_store = SqliteStore::temporary().await;
338        let bobby_store = SqliteStore::temporary().await;
339        let cathy_store = SqliteStore::temporary().await;
340
341        let alice_device_group = SigningKey::generate().verifying_key();
342        let bobby_device_group = SigningKey::generate().verifying_key();
343        let cathy_device_group = SigningKey::generate().verifying_key();
344
345        let ab_chat = SigningKey::generate().verifying_key();
346        let bc_chat = SigningKey::generate().verifying_key();
347
348        let alice_ingest = Ingest::new(alice_store.clone());
349        let bobby_ingest = Ingest::new(bobby_store.clone());
350        let cathy_ingest = Ingest::new(cathy_store.clone());
351
352        let alice_groups = Groups::new(alice_store.clone());
353        let bobby_groups = Groups::new(bobby_store.clone());
354        let cathy_groups = Groups::new(cathy_store.clone());
355
356        // All members create their own device groups and process them on their own stores.
357
358        let args = GroupsExtensionArgs {
359            group_id: alice_device_group,
360            action: GroupAction::Create {
361                initial_members: vec![(GroupMember::Individual(alice), Access::manage())],
362            },
363            dependencies: vec![],
364        };
365
366        let create_alice_device_00: Operation<TestExtensions> =
367            alice_log.operation(&[], TestExtensions::from(args));
368
369        let event_00 = Event::from(create_alice_device_00.clone());
370
371        alice_ingest.process(event_00.clone()).await.unwrap();
372        alice_groups.process(event_00).await.unwrap();
373
374        let args = GroupsExtensionArgs {
375            group_id: bobby_device_group,
376            action: GroupAction::Create {
377                initial_members: vec![(GroupMember::Individual(bobby), Access::manage())],
378            },
379            dependencies: vec![],
380        };
381        let create_bobby_device_01: Operation<TestExtensions> =
382            bobby_log.operation(&[], TestExtensions::from(args));
383        let event_01 = Event::from(create_bobby_device_01.clone());
384
385        bobby_ingest.process(event_01.clone()).await.unwrap();
386        bobby_groups.process(event_01.clone()).await.unwrap();
387
388        let args = GroupsExtensionArgs {
389            group_id: cathy_device_group,
390            action: GroupAction::Create {
391                initial_members: vec![(GroupMember::Individual(cathy), Access::manage())],
392            },
393            dependencies: vec![],
394        };
395        let create_cathy_device_02: Operation<TestExtensions> =
396            cathy_log.operation(&[], TestExtensions::from(args));
397        let event_02 = Event::from(create_cathy_device_02.clone());
398
399        cathy_ingest.process(event_02.clone()).await.unwrap();
400        cathy_groups.process(event_02).await.unwrap();
401
402        // Alice creates chat with Bobby.
403        //
404        // First they process "create device group" operation from Bobby.
405        alice_ingest.process(event_01.clone()).await.unwrap();
406        alice_groups.process(event_01.clone()).await.unwrap();
407
408        // Then they create the chat group.
409        let y: GroupsState = tx_unwrap!(alice_store, {
410            let state_id = Hash::digest("default");
411            alice_store
412                .get_groups_state_tx(state_id)
413                .await
414                .unwrap()
415                .unwrap()
416        });
417
418        let args = GroupsExtensionArgs {
419            group_id: ab_chat,
420            action: GroupAction::Create {
421                initial_members: vec![
422                    (GroupMember::Group(alice_device_group), Access::write()),
423                    (GroupMember::Group(bobby_device_group), Access::write()),
424                ],
425            },
426            dependencies: y.heads_filtered(&[alice_device_group, bobby_device_group]),
427        };
428        let create_alice_bobby_chat_03: Operation<TestExtensions> =
429            alice_log.operation(&[], TestExtensions::from(args));
430        let event_03 = Event::from(create_alice_bobby_chat_03.clone());
431
432        alice_ingest.process(event_03.clone()).await.unwrap();
433        alice_groups.process(event_03).await.unwrap();
434
435        // Bobby processes alice's "create device group" and "create ab chat".
436        for op in [create_alice_device_00.clone(), create_alice_bobby_chat_03] {
437            let event: Event = op.into();
438
439            bobby_ingest.process(event.clone()).await.unwrap();
440            bobby_groups.process(event).await.unwrap();
441        }
442
443        // Both Alice and Bobby have the correct groups state.
444        for store in [alice_store.clone(), bobby_store.clone()] {
445            let y: GroupsState = tx_unwrap!(store, {
446                let state_id = Hash::digest("default");
447                store.get_groups_state_tx(state_id).await.unwrap().unwrap()
448            });
449
450            let mut members = y.members(ab_chat);
451            members.sort();
452
453            assert_eq!(members.len(), 2);
454            assert!(members.contains(&(alice, Access::write())));
455            assert!(members.contains(&(bobby, Access::write())));
456        }
457
458        // Cathy now creates a chat with Bobby.
459        //
460        // First they process "create device group" for bobby.
461        cathy_ingest.process(event_01.clone()).await.unwrap();
462        cathy_groups.process(event_01).await.unwrap();
463
464        // Then they create the chat group.
465        let y: GroupsState = tx_unwrap!(cathy_store, {
466            let state_id = Hash::digest("default");
467            cathy_store
468                .get_groups_state_tx(state_id)
469                .await
470                .unwrap()
471                .unwrap()
472        });
473
474        let args = GroupsExtensionArgs {
475            group_id: bc_chat,
476            action: GroupAction::Create {
477                initial_members: vec![
478                    (GroupMember::Group(bobby_device_group), Access::write()),
479                    (GroupMember::Group(cathy_device_group), Access::write()),
480                ],
481            },
482            dependencies: y.heads_filtered(&[bobby_device_group, cathy_device_group]),
483        };
484        let create_bobby_cathy_chat_04: Operation<TestExtensions> =
485            cathy_log.operation(&[], TestExtensions::from(args));
486        let event_04 = Event::from(create_bobby_cathy_chat_04.clone());
487
488        cathy_ingest.process(event_04.clone()).await.unwrap();
489        cathy_groups.process(event_04).await.unwrap();
490
491        // Bobby processes cathy's "create device group" and "create bc chat".
492        for op in [create_cathy_device_02.clone(), create_bobby_cathy_chat_04] {
493            let event: Event = op.into();
494
495            bobby_ingest.process(event.clone()).await.unwrap();
496            bobby_groups.process(event).await.unwrap();
497        }
498
499        // Both Cathy and Bobby have the correct groups state.
500        for store in [cathy_store, bobby_store] {
501            let y: GroupsState = tx_unwrap!(store, {
502                let state_id = Hash::digest("default");
503                store.get_groups_state_tx(state_id).await.unwrap().unwrap()
504            });
505            let mut members = y.members(bc_chat);
506            members.sort();
507
508            assert_eq!(members.len(), 2);
509            assert!(members.contains(&(bobby, Access::write())));
510            assert!(members.contains(&(cathy, Access::write())));
511        }
512    }
513}