Skip to main content

saorsa_core/threshold/
group.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: david@saorsalabs.com
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under these licenses is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
14//! Threshold group management operations
15
16use super::*;
17use crate::quantum_crypto::types::*;
18use std::collections::HashSet;
19
20impl ThresholdGroup {
21    /// Check if a participant has a specific permission
22    pub fn check_permission(
23        &self,
24        participant_id: &ParticipantId,
25        permission: Permission,
26    ) -> Result<()> {
27        let participant = self
28            .active_participants
29            .iter()
30            .find(|p| &p.participant_id == participant_id)
31            .ok_or_else(|| ThresholdError::ParticipantNotFound(participant_id.clone()))?;
32
33        match (&participant.role, permission) {
34            (ParticipantRole::Leader { permissions }, Permission::AddParticipant) => {
35                if permissions.can_add_participants {
36                    Ok(())
37                } else {
38                    Err(ThresholdError::Unauthorized(
39                        "Cannot add participants".to_string(),
40                    ))
41                }
42            }
43            (ParticipantRole::Leader { permissions }, Permission::RemoveParticipant) => {
44                if permissions.can_remove_participants {
45                    Ok(())
46                } else {
47                    Err(ThresholdError::Unauthorized(
48                        "Cannot remove participants".to_string(),
49                    ))
50                }
51            }
52            (ParticipantRole::Leader { permissions }, Permission::UpdateThreshold) => {
53                if permissions.can_update_threshold {
54                    Ok(())
55                } else {
56                    Err(ThresholdError::Unauthorized(
57                        "Cannot update threshold".to_string(),
58                    ))
59                }
60            }
61            (ParticipantRole::Member { permissions }, Permission::Sign) => {
62                if permissions.can_sign {
63                    Ok(())
64                } else {
65                    Err(ThresholdError::Unauthorized("Cannot sign".to_string()))
66                }
67            }
68            (ParticipantRole::Observer, _) => Err(ThresholdError::Unauthorized(
69                "Observers have read-only access".to_string(),
70            )),
71            _ => Err(ThresholdError::Unauthorized(
72                "Permission denied".to_string(),
73            )),
74        }
75    }
76
77    /// Get active participants (not suspended or pending removal)
78    pub fn get_active_participants(&self) -> Vec<&ParticipantInfo> {
79        self.active_participants
80            .iter()
81            .filter(|p| matches!(p.status, ParticipantStatus::Active))
82            .collect()
83    }
84
85    /// Get number of active participants
86    pub fn active_participant_count(&self) -> u16 {
87        self.get_active_participants().len() as u16
88    }
89
90    /// Check if we have enough participants for threshold operations
91    pub fn has_threshold_participants(&self) -> bool {
92        self.active_participant_count() >= self.threshold
93    }
94
95    /// Add a new participant (pending until key ceremony)
96    pub fn add_pending_participant(&mut self, participant: ParticipantInfo) -> Result<()> {
97        // Check if participant already exists
98        if self
99            .active_participants
100            .iter()
101            .any(|p| p.participant_id == participant.participant_id)
102        {
103            return Err(ThresholdError::InvalidParameters(
104                "Participant already exists".to_string(),
105            ));
106        }
107
108        if self
109            .pending_participants
110            .iter()
111            .any(|p| p.participant_id == participant.participant_id)
112        {
113            return Err(ThresholdError::InvalidParameters(
114                "Participant already pending".to_string(),
115            ));
116        }
117
118        self.pending_participants.push(participant);
119        self.version += 1;
120        self.last_updated = SystemTime::now();
121
122        Ok(())
123    }
124
125    /// Mark participant for removal
126    pub fn mark_for_removal(&mut self, participant_id: &ParticipantId) -> Result<()> {
127        let participant = self
128            .active_participants
129            .iter_mut()
130            .find(|p| &p.participant_id == participant_id)
131            .ok_or_else(|| ThresholdError::ParticipantNotFound(participant_id.clone()))?;
132
133        participant.status = ParticipantStatus::PendingRemoval;
134        self.version += 1;
135        self.last_updated = SystemTime::now();
136
137        // Check if we still have enough participants
138        if self.active_participant_count() < self.threshold {
139            return Err(ThresholdError::InsufficientParticipants {
140                required: self.threshold,
141                available: self.active_participant_count(),
142            });
143        }
144
145        Ok(())
146    }
147
148    /// Update participant role
149    pub fn update_participant_role(
150        &mut self,
151        participant_id: &ParticipantId,
152        new_role: ParticipantRole,
153    ) -> Result<()> {
154        let participant = self
155            .active_participants
156            .iter_mut()
157            .find(|p| &p.participant_id == participant_id)
158            .ok_or_else(|| ThresholdError::ParticipantNotFound(participant_id.clone()))?;
159
160        participant.role = new_role;
161        self.version += 1;
162        self.last_updated = SystemTime::now();
163
164        Ok(())
165    }
166
167    /// Suspend a participant
168    pub fn suspend_participant(
169        &mut self,
170        participant_id: &ParticipantId,
171        reason: String,
172        duration: std::time::Duration,
173    ) -> Result<()> {
174        let participant = self
175            .active_participants
176            .iter_mut()
177            .find(|p| &p.participant_id == participant_id)
178            .ok_or_else(|| ThresholdError::ParticipantNotFound(participant_id.clone()))?;
179
180        participant.status = ParticipantStatus::Suspended {
181            reason,
182            until: SystemTime::now() + duration,
183        };
184
185        self.version += 1;
186        self.last_updated = SystemTime::now();
187
188        // Check if we still have enough participants
189        if self.active_participant_count() < self.threshold {
190            return Err(ThresholdError::InsufficientParticipants {
191                required: self.threshold,
192                available: self.active_participant_count(),
193            });
194        }
195
196        Ok(())
197    }
198
199    /// Update threshold value
200    pub fn update_threshold(&mut self, new_threshold: u16) -> Result<()> {
201        if new_threshold == 0 {
202            return Err(ThresholdError::InvalidParameters(
203                "Threshold must be at least 1".to_string(),
204            ));
205        }
206
207        if new_threshold > self.participants {
208            return Err(ThresholdError::InvalidParameters(
209                "Threshold cannot exceed total participants".to_string(),
210            ));
211        }
212
213        if new_threshold > self.active_participant_count() {
214            return Err(ThresholdError::InvalidParameters(
215                "Threshold cannot exceed active participants".to_string(),
216            ));
217        }
218
219        self.threshold = new_threshold;
220        self.version += 1;
221        self.last_updated = SystemTime::now();
222
223        Ok(())
224    }
225
226    /// Get participants by role
227    pub fn get_participants_by_role(&self, role_filter: RoleFilter) -> Vec<&ParticipantInfo> {
228        self.active_participants
229            .iter()
230            .filter(|p| {
231                matches!(
232                    (&p.role, &role_filter),
233                    (ParticipantRole::Leader { .. }, RoleFilter::Leaders)
234                        | (ParticipantRole::Member { .. }, RoleFilter::Members)
235                        | (ParticipantRole::Observer, RoleFilter::Observers)
236                        | (_, RoleFilter::All)
237                )
238            })
239            .collect()
240    }
241
242    /// Get group hierarchy (if part of a larger structure)
243    pub fn get_hierarchy(&self) -> GroupHierarchy {
244        GroupHierarchy {
245            group_id: self.group_id.clone(),
246            parent: self.metadata.parent_group.clone(),
247            name: self.metadata.name.clone(),
248            threshold: self.threshold,
249            participants: self.participants,
250            purpose: self.metadata.purpose.clone(),
251        }
252    }
253
254    /// Validate group state
255    pub fn validate(&self) -> Result<()> {
256        // Check basic constraints
257        if self.threshold == 0 {
258            return Err(ThresholdError::InvalidParameters(
259                "Invalid threshold: must be at least 1".to_string(),
260            ));
261        }
262
263        if self.threshold > self.participants {
264            return Err(ThresholdError::InvalidParameters(
265                "Invalid threshold: exceeds total participants".to_string(),
266            ));
267        }
268
269        // Check for duplicate participant IDs
270        let mut seen_ids = HashSet::new();
271        for participant in &self.active_participants {
272            if !seen_ids.insert(&participant.participant_id) {
273                return Err(ThresholdError::InvalidParameters(format!(
274                    "Duplicate participant ID: {:?}",
275                    participant.participant_id
276                )));
277            }
278        }
279
280        // Verify we have at least one leader
281        let has_leader = self
282            .active_participants
283            .iter()
284            .any(|p| matches!(p.role, ParticipantRole::Leader { .. }));
285
286        if !has_leader {
287            return Err(ThresholdError::InvalidParameters(
288                "Group must have at least one leader".to_string(),
289            ));
290        }
291
292        Ok(())
293    }
294
295    /// Add audit entry
296    pub fn add_audit_entry(&mut self, entry: GroupAuditEntry) {
297        self.audit_log.push(entry);
298
299        // Keep audit log size reasonable (last 1000 entries)
300        if self.audit_log.len() > 1000 {
301            self.audit_log.drain(0..100);
302        }
303    }
304}
305
306/// Permission types
307#[derive(Debug, Clone, Copy, PartialEq)]
308pub enum Permission {
309    AddParticipant,
310    RemoveParticipant,
311    UpdateThreshold,
312    Sign,
313    Vote,
314    CreateSubgroup,
315    AssignRoles,
316}
317
318/// Role filter for queries
319#[derive(Debug, Clone, PartialEq)]
320pub enum RoleFilter {
321    All,
322    Leaders,
323    Members,
324    Observers,
325}
326
327/// Group hierarchy information
328#[derive(Debug, Clone)]
329pub struct GroupHierarchy {
330    pub group_id: GroupId,
331    pub parent: Option<GroupId>,
332    pub name: String,
333    pub threshold: u16,
334    pub participants: u16,
335    pub purpose: GroupPurpose,
336}
337
338/// Group statistics
339#[derive(Debug, Clone)]
340pub struct GroupStats {
341    pub total_participants: u16,
342    pub active_participants: u16,
343    pub pending_participants: u16,
344    pub suspended_participants: u16,
345    pub leaders: u16,
346    pub members: u16,
347    pub observers: u16,
348    pub total_operations: usize,
349    pub successful_operations: usize,
350    pub failed_operations: usize,
351}
352
353impl ThresholdGroup {
354    /// Get group statistics
355    pub fn get_stats(&self) -> GroupStats {
356        let mut stats = GroupStats {
357            total_participants: self.participants,
358            active_participants: 0,
359            pending_participants: self.pending_participants.len() as u16,
360            suspended_participants: 0,
361            leaders: 0,
362            members: 0,
363            observers: 0,
364            total_operations: self.audit_log.len(),
365            successful_operations: 0,
366            failed_operations: 0,
367        };
368
369        for participant in &self.active_participants {
370            match &participant.status {
371                ParticipantStatus::Active => stats.active_participants += 1,
372                ParticipantStatus::Suspended { .. } => stats.suspended_participants += 1,
373                _ => {}
374            }
375
376            match &participant.role {
377                ParticipantRole::Leader { .. } => stats.leaders += 1,
378                ParticipantRole::Member { .. } => stats.members += 1,
379                ParticipantRole::Observer => stats.observers += 1,
380            }
381        }
382
383        for entry in &self.audit_log {
384            match &entry.result {
385                OperationResult::Success => stats.successful_operations += 1,
386                OperationResult::Failed(_) => stats.failed_operations += 1,
387                OperationResult::Pending => {}
388            }
389        }
390
391        stats
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    fn create_test_group() -> ThresholdGroup {
400        let participant1 = ParticipantInfo {
401            participant_id: ParticipantId(1),
402            public_key: vec![1; 32],
403            frost_share_commitment: FrostCommitment(vec![1; 32]),
404            role: ParticipantRole::Leader {
405                permissions: LeaderPermissions::default(),
406            },
407            status: ParticipantStatus::Active,
408            joined_at: SystemTime::now(),
409            metadata: HashMap::new(),
410        };
411
412        let participant2 = ParticipantInfo {
413            participant_id: ParticipantId(2),
414            public_key: vec![2; 32],
415            frost_share_commitment: FrostCommitment(vec![2; 32]),
416            role: ParticipantRole::Member {
417                permissions: MemberPermissions::default(),
418            },
419            status: ParticipantStatus::Active,
420            joined_at: SystemTime::now(),
421            metadata: HashMap::new(),
422        };
423
424        ThresholdGroup {
425            group_id: GroupId([0; 32]),
426            threshold: 2,
427            participants: 2,
428            frost_group_key: FrostGroupPublicKey(vec![0; 32]),
429            active_participants: vec![participant1, participant2],
430            pending_participants: vec![],
431            version: 1,
432            metadata: GroupMetadata {
433                name: "Test Group".to_string(),
434                description: "Test group for unit tests".to_string(),
435                purpose: GroupPurpose::MultiSig,
436                parent_group: None,
437                custom_data: HashMap::new(),
438            },
439            audit_log: vec![],
440            created_at: SystemTime::now(),
441            last_updated: SystemTime::now(),
442        }
443    }
444
445    #[test]
446    fn test_permission_checking() {
447        let group = create_test_group();
448
449        // Leader can add participants
450        assert!(
451            group
452                .check_permission(&ParticipantId(1), Permission::AddParticipant)
453                .is_ok()
454        );
455
456        // Member cannot add participants
457        assert!(
458            group
459                .check_permission(&ParticipantId(2), Permission::AddParticipant)
460                .is_err()
461        );
462
463        // Member can sign
464        assert!(
465            group
466                .check_permission(&ParticipantId(2), Permission::Sign)
467                .is_ok()
468        );
469    }
470
471    #[test]
472    fn test_group_validation() {
473        let mut group = create_test_group();
474
475        // Valid group
476        assert!(group.validate().is_ok());
477
478        // Invalid threshold
479        group.threshold = 0;
480        assert!(group.validate().is_err());
481
482        group.threshold = 3; // More than participants
483        assert!(group.validate().is_err());
484    }
485
486    #[test]
487    fn test_participant_management() {
488        let mut group = create_test_group();
489
490        // Add pending participant
491        let new_participant = ParticipantInfo {
492            participant_id: ParticipantId(3),
493            public_key: vec![3; 32],
494            frost_share_commitment: FrostCommitment(vec![3; 32]),
495            role: ParticipantRole::Member {
496                permissions: MemberPermissions::default(),
497            },
498            status: ParticipantStatus::PendingJoin,
499            joined_at: SystemTime::now(),
500            metadata: HashMap::new(),
501        };
502
503        assert!(group.add_pending_participant(new_participant).is_ok());
504        assert_eq!(group.pending_participants.len(), 1);
505
506        // Cannot add duplicate
507        let duplicate = ParticipantInfo {
508            participant_id: ParticipantId(1),
509            public_key: vec![1; 32],
510            frost_share_commitment: FrostCommitment(vec![1; 32]),
511            role: ParticipantRole::Member {
512                permissions: MemberPermissions::default(),
513            },
514            status: ParticipantStatus::PendingJoin,
515            joined_at: SystemTime::now(),
516            metadata: HashMap::new(),
517        };
518
519        assert!(group.add_pending_participant(duplicate).is_err());
520    }
521}