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