pub struct GroupState {
pub group_id: MessageId,
pub name: String,
pub settings: GroupSettings,
pub metadata: Vec<Metadata>,
pub members: BTreeMap<IdentityRef, GroupMember>,
pub membership: GroupMembership,
pub event_history: Vec<MessageId>,
pub last_event_timestamp: u64,
pub version: u64,
}Expand description
The complete runtime state of a distributed encrypted group.
GroupState represents the unified, authoritative state of a group at any point in time.
It combines immutable group information (from super::events::GroupInfo) with runtime
state such as active members, event history, and identity management.
ยง๐๏ธ Design Philosophy
This type unifies what were previously separate concerns:
- Static Group Information: Name, settings, and structured metadata
- Dynamic Member State: Active participants, roles, and activity tracking
- Event History: Audit trail and conflict resolution capability
- Identity Management: Complex alias and display name handling
ยง๐ Event-Sourced Architecture
Groups maintain state through event sourcing:
CreateGroup Event โ Initial GroupState
โ
Member Activity โ Updated GroupState (new member added)
โ
Role Assignment โ Updated GroupState (permissions changed)
โ
Group Update โ Updated GroupState (metadata modified)Each event is applied via GroupState::apply_event, ensuring consistency
and providing an audit trail through GroupState::event_history.
ยง๐ Security and Access Control
ยงEncryption-Based Membership
- Anyone with the groupโs encryption key can participate
GroupState::memberstracks known active participants, not access control- True access control is enforced by possession of the encryption key
ยงRole-Based Permissions
- Each member has a
super::events::roles::GroupRoledefining their capabilities - Permissions are checked via
GroupState::check_permission - Role assignments are cryptographically signed and part of the event history
ยงIdentity Privacy
- Members can use aliases within groups via
GroupMembership - Display names can be set independently of cryptographic identities
- Multiple aliases per
zoe_wire_protocol::VerifyingKeyare supported
ยง๐ Member Lifecycle
- Discovery: A user obtains the group encryption key through some secure channel
- Announcement: User sends any
super::events::GroupActivityEventto announce participation - Recognition: Internal handling adds them to active member list
- Activity: Memberโs
GroupMember::last_activeis updated with each message - Departure:
super::events::GroupActivityEvent::LeaveGroupremoves from active list
Note: Departure only removes from the active member tracking - the user still possesses the encryption key and could rejoin at any time.
ยง๐ท๏ธ Structured Metadata System
Metadata is stored as crate::Metadata variants rather than simple key-value pairs:
crate::Metadata::Description: Human-readable group descriptioncrate::Metadata::Generic: Key-value pairs for backward compatibility- Future variants can add typed metadata (images, files, etc.)
Use GroupState::description() and GroupState::generic_metadata() for
convenient access to common metadata patterns.
ยง๐ Relationship to GroupInfo
super::events::GroupInfo is used for events (creation, updates) while
GroupState represents the current runtime state:
GroupInfo (in events) โ GroupState (runtime) โ GroupInfo (for updates)Use GroupState::from_group_info and GroupState::to_group_info to
convert between representations.
ยง๐ก Usage Examples
ยงCreating a Group State
use zoe_app_primitives::{GroupState, GroupSettings, Metadata};
use zoe_wire_protocol::KeyPair;
use blake3::Hash;
let creator_key = KeyPair::generate(&mut rand::rngs::OsRng);
let group_id = Hash::from([1u8; 32]);
let metadata = vec![
Metadata::Description("Development team coordination".to_string()),
Metadata::Generic { key: "department".to_string(), value: "engineering".to_string() },
];
let group_state = GroupState::new(
group_id,
"Dev Team".to_string(),
GroupSettings::default(),
metadata,
creator_key.public_key(),
1234567890,
);
// Creator is automatically added as Owner
assert_eq!(group_state.members.len(), 1);
assert!(group_state.is_member(&creator_key.public_key()));ยงProcessing Member Activity
let new_member = KeyPair::generate(&mut rand::rngs::OsRng);
let activity_event = GroupActivityEvent::Activity(());
// New member announces participation
group_state.apply_event(
&activity_event,
Hash::from([2u8; 32]),
new_member.public_key(),
1234567891,
).unwrap();
// They're now tracked as an active member
assert!(group_state.is_member(&new_member.public_key()));ยงWorking with Metadata
// Extract specific metadata types
assert_eq!(group_state.description(), Some("Test group".to_string()));
// Get all generic metadata as a map
let generic_meta = group_state.generic_metadata();Fieldsยง
ยงgroup_id: MessageIdThe group identifier - this is the Blake3 hash of the CreateGroup message Also serves as the root event ID (used as channel tag)
name: StringCurrent group name
settings: GroupSettingsCurrent group settings
metadata: Vec<Metadata>Group metadata as structured types
members: BTreeMap<IdentityRef, GroupMember>Runtime member state with roles and activity tracking Keys are ML-DSA verifying keys encoded as bytes for serialization compatibility
membership: GroupMembershipAdvanced identity management for aliases and display names
event_history: Vec<MessageId>Event history for this group (event ID -> event details)
last_event_timestamp: u64Last processed event timestamp (for ordering)
version: u64State version (incremented on each event)
Implementationsยง
Sourceยงimpl GroupState
impl GroupState
Sourcepub fn new(
group_id: MessageId,
name: String,
settings: GroupSettings,
metadata: Vec<Metadata>,
creator: VerifyingKey,
timestamp: u64,
) -> GroupState
pub fn new( group_id: MessageId, name: String, settings: GroupSettings, metadata: Vec<Metadata>, creator: VerifyingKey, timestamp: u64, ) -> GroupState
Create a new group state from a group creation event.
This constructor sets up the initial state for a newly created group, including:
- Setting the creator as the first member with
GroupRole::Ownerrole - Initializing empty membership state for identity management
- Recording the group creation as the first event in history
- Setting initial timestamps and version number
ยงArguments
group_id- Blake3 hash of the group creation message (also serves as root event ID)name- Human-readable group namesettings- Group configuration and permissionsmetadata- Structured metadata usingcrate::Metadatatypescreator- Public key of the group creator (becomes first Owner)timestamp- Unix timestamp of group creation
ยงReturns
A new GroupState with the creator as the sole member and owner.
ยงExamples
use zoe_app_primitives::{GroupState, GroupSettings, Metadata, events::roles::GroupRole};
use zoe_wire_protocol::KeyPair;
use blake3::Hash;
let creator_key = KeyPair::generate(&mut rand::rngs::OsRng);
let group_id = Hash::from([42u8; 32]);
let metadata = vec![
Metadata::Description("Team coordination space".to_string()),
Metadata::Generic { key: "project".to_string(), value: "zoe-chat".to_string() },
];
let group_state = GroupState::new(
group_id,
"Engineering Team".to_string(),
GroupSettings::default(),
metadata,
creator_key.public_key(),
1640995200, // 2022-01-01 00:00:00 UTC
);
// Verify initial state
assert_eq!(group_state.name, "Engineering Team");
assert_eq!(group_state.members.len(), 1);
assert_eq!(group_state.version, 1);
assert!(group_state.is_member(&creator_key.public_key()));
assert_eq!(
group_state.member_role(&creator_key.public_key()),
Some(&GroupRole::Owner)
);Sourcepub fn from_group_info(
group_id: MessageId,
group_info: &GroupInfo,
creator: VerifyingKey,
timestamp: u64,
) -> GroupState
pub fn from_group_info( group_id: MessageId, group_info: &GroupInfo, creator: VerifyingKey, timestamp: u64, ) -> GroupState
Create a GroupState from existing GroupInfo (for compatibility)
Sourcepub fn to_group_info(&self, key_info: GroupKeyInfo) -> GroupInfo
pub fn to_group_info(&self, key_info: GroupKeyInfo) -> GroupInfo
Convert to GroupInfo for events (extracts the core group information)
Sourcepub fn apply_event<T>(
&mut self,
event: &GroupActivityEvent<T>,
event_id: MessageId,
sender: VerifyingKey,
timestamp: u64,
) -> Result<(), GroupStateError>
pub fn apply_event<T>( &mut self, event: &GroupActivityEvent<T>, event_id: MessageId, sender: VerifyingKey, timestamp: u64, ) -> Result<(), GroupStateError>
Apply an event to this group state, updating it according to event-sourced principles.
This is the core method for updating group state. All state changes must go through this method to ensure consistency, proper ordering, and audit trail maintenance. Events are applied in chronological order to maintain deterministic state.
ยงEvent Processing
The method handles several types of events:
- Member Activity: Any activity announces participation and updates last_active
- Role Changes: Updates member roles and permissions
- Group Updates: Modifies name, settings, and metadata
- Member Departure: Removes members from active tracking
- Identity Management: Processes identity declarations and updates
ยงOrdering and Consistency
Events must be applied in timestamp order. The method will reject events with timestamps older than the last processed event to maintain consistency across all group participants.
ยงArguments
event- The group activity event to processevent_id- Blake3 hash of the event message (for audit trail)sender- Public key of the event sender (for authorization)timestamp- Unix timestamp of the event (for ordering)
ยงReturns
Ok(()) if the event was successfully applied, or GroupStateError if:
- Event timestamp is out of order
- Sender lacks required permissions
- Member is not found for role operations
- Other validation failures
ยงExamples
use zoe_app_primitives::{GroupState, GroupActivityEvent, GroupSettings, Metadata};
use zoe_wire_protocol::KeyPair;
use blake3::Hash;
let creator_key = KeyPair::generate(&mut rand::rngs::OsRng);
let new_member_key = KeyPair::generate(&mut rand::rngs::OsRng);
let mut group_state = GroupState::new(
Hash::from([1u8; 32]),
"Test Group".to_string(),
GroupSettings::default(),
vec![],
creator_key.public_key(),
1000,
);
// New member announces participation via activity
let activity_event = GroupActivityEvent::Activity(());
let event_id = Hash::from([2u8; 32]);
group_state.apply_event(
&activity_event,
event_id,
new_member_key.public_key(),
1001, // Must be after creation timestamp
).unwrap();
// Member is now tracked in the group
assert!(group_state.is_member(&new_member_key.public_key()));
assert_eq!(group_state.members.len(), 2); // Creator + new member
assert_eq!(group_state.version, 2); // Version incremented
assert_eq!(group_state.event_history.len(), 2); // Event recordedยงState Transitions
After each successful event application:
GroupState::versionis incrementedGroupState::last_event_timestampis updatedGroupState::event_historyincludes the new event ID- Specific state changes depend on the event type
Sourcepub fn check_permission(
&self,
member: &VerifyingKey,
required_permission: &Permission,
) -> Result<(), GroupStateError>
pub fn check_permission( &self, member: &VerifyingKey, required_permission: &Permission, ) -> Result<(), GroupStateError>
Check if a member has permission to perform an action
Sourcepub fn get_members(&self) -> &BTreeMap<IdentityRef, GroupMember>
pub fn get_members(&self) -> &BTreeMap<IdentityRef, GroupMember>
Get all active members
Sourcepub fn is_member(&self, user: &VerifyingKey) -> bool
pub fn is_member(&self, user: &VerifyingKey) -> bool
Check if a user is a member of this group
Sourcepub fn member_role(&self, user: &VerifyingKey) -> Option<&GroupRole>
pub fn member_role(&self, user: &VerifyingKey) -> Option<&GroupRole>
Get a memberโs role
Sourcepub fn description(&self) -> Option<String>
pub fn description(&self) -> Option<String>
Extract the group description from structured metadata.
This method searches through the structured crate::Metadata collection
to find a crate::Metadata::Description variant and returns its value.
This provides a convenient way to access the primary descriptive text
for the group.
ยงReturns
Some(description) if a description metadata entry exists, None otherwise.
ยงExamples
use zoe_app_primitives::{GroupState, GroupSettings, Metadata};
use zoe_wire_protocol::KeyPair;
use blake3::Hash;
let creator_key = KeyPair::generate(&mut rand::rngs::OsRng);
// Group with description
let metadata_with_desc = vec![
Metadata::Description("A team coordination space".to_string()),
Metadata::Generic { key: "category".to_string(), value: "work".to_string() },
];
let group_state = GroupState::new(
Hash::from([1u8; 32]),
"Team Chat".to_string(),
GroupSettings::default(),
metadata_with_desc,
creator_key.public_key(),
1000,
);
assert_eq!(
group_state.description(),
Some("A team coordination space".to_string())
);
// Group without description
let metadata_no_desc = vec![
Metadata::Generic { key: "category".to_string(), value: "work".to_string() },
];
let group_state_no_desc = GroupState::new(
Hash::from([2u8; 32]),
"Another Group".to_string(),
GroupSettings::default(),
metadata_no_desc,
creator_key.public_key(),
1000,
);
assert_eq!(group_state_no_desc.description(), None);Sourcepub fn generic_metadata(&self) -> BTreeMap<String, String>
pub fn generic_metadata(&self) -> BTreeMap<String, String>
Extract generic key-value metadata as a BTreeMap for backward compatibility.
This method filters the structured crate::Metadata collection to extract
only the crate::Metadata::Generic variants and returns them as a
std::collections::BTreeMap. This provides compatibility with code that
expects simple key-value metadata storage.
ยงStructured vs Generic Metadata
The group system supports both structured metadata (typed variants like
crate::Metadata::Description) and generic key-value pairs. This method
extracts only the generic pairs, ignoring other metadata types.
ยงReturns
A std::collections::BTreeMap containing all generic metadata key-value pairs.
The map will be empty if no generic metadata exists.
ยงExamples
use zoe_app_primitives::{GroupState, GroupSettings, Metadata};
use zoe_wire_protocol::KeyPair;
use blake3::Hash;
let creator_key = KeyPair::generate(&mut rand::rngs::OsRng);
let metadata = vec![
Metadata::Description("Team workspace".to_string()), // Not included in generic
Metadata::Generic { key: "department".to_string(), value: "engineering".to_string() },
Metadata::Generic { key: "project".to_string(), value: "zoe-chat".to_string() },
Metadata::Generic { key: "visibility".to_string(), value: "internal".to_string() },
];
let group_state = GroupState::new(
Hash::from([1u8; 32]),
"Engineering Team".to_string(),
GroupSettings::default(),
metadata,
creator_key.public_key(),
1000,
);
let generic_meta = group_state.generic_metadata();
// Only generic metadata is included (3 items, description excluded)
assert_eq!(generic_meta.len(), 3);
assert_eq!(generic_meta.get("department"), Some(&"engineering".to_string()));
assert_eq!(generic_meta.get("project"), Some(&"zoe-chat".to_string()));
assert_eq!(generic_meta.get("visibility"), Some(&"internal".to_string()));
// Description is not in generic metadata
assert!(!generic_meta.contains_key("description"));
// But it's still accessible via the description() method
assert_eq!(
group_state.description(),
Some("Team workspace".to_string())
);ยงUse Cases
This method is particularly useful for:
- Legacy Code Integration: Existing code expecting simple key-value metadata
- Generic Queries: Searching through all key-value pairs programmatically
- Serialization: Converting to formats that donโt support structured metadata
- Configuration: Accessing arbitrary configuration key-value pairs
Trait Implementationsยง
Sourceยงimpl Clone for GroupState
impl Clone for GroupState
Sourceยงfn clone(&self) -> GroupState
fn clone(&self) -> GroupState
1.0.0 ยท Sourceยงfn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more