GroupState

Struct GroupState 

Source
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::members tracks known active participants, not access control
  • True access control is enforced by possession of the encryption key

ยงRole-Based Permissions

ยงIdentity Privacy

ยง๐Ÿ“Š Member Lifecycle

  1. Discovery: A user obtains the group encryption key through some secure channel
  2. Announcement: User sends any super::events::GroupActivityEvent to announce participation
  3. Recognition: Internal handling adds them to active member list
  4. Activity: Memberโ€™s GroupMember::last_active is updated with each message
  5. Departure: super::events::GroupActivityEvent::LeaveGroup removes 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:

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: MessageId

The group identifier - this is the Blake3 hash of the CreateGroup message Also serves as the root event ID (used as channel tag)

ยงname: String

Current group name

ยงsettings: GroupSettings

Current 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: GroupMembership

Advanced identity management for aliases and display names

ยงevent_history: Vec<MessageId>

Event history for this group (event ID -> event details)

ยงlast_event_timestamp: u64

Last processed event timestamp (for ordering)

ยงversion: u64

State version (incremented on each event)

Implementationsยง

Sourceยง

impl GroupState

Source

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::Owner role
  • 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 name
  • settings - Group configuration and permissions
  • metadata - Structured metadata using crate::Metadata types
  • creator - 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)
);
Source

pub fn from_group_info( group_id: MessageId, group_info: &GroupInfo, creator: VerifyingKey, timestamp: u64, ) -> GroupState

Create a GroupState from existing GroupInfo (for compatibility)

Source

pub fn to_group_info(&self, key_info: GroupKeyInfo) -> GroupInfo

Convert to GroupInfo for events (extracts the core group information)

Source

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 process
  • event_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:

Source

pub fn check_permission( &self, member: &VerifyingKey, required_permission: &Permission, ) -> Result<(), GroupStateError>

Check if a member has permission to perform an action

Source

pub fn get_members(&self) -> &BTreeMap<IdentityRef, GroupMember>

Get all active members

Source

pub fn is_member(&self, user: &VerifyingKey) -> bool

Check if a user is a member of this group

Source

pub fn member_role(&self, user: &VerifyingKey) -> Option<&GroupRole>

Get a memberโ€™s role

Source

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);
Source

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

Sourceยง

fn clone(&self) -> GroupState

Returns a duplicate of the value. Read more
1.0.0 ยท Sourceยง

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Sourceยง

impl Debug for GroupState

Sourceยง

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Sourceยง

impl<'de> Deserialize<'de> for GroupState

Sourceยง

fn deserialize<__D>( __deserializer: __D, ) -> Result<GroupState, <__D as Deserializer<'de>>::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Sourceยง

impl Serialize for GroupState

Sourceยง

fn serialize<__S>( &self, __serializer: __S, ) -> Result<<__S as Serializer>::Ok, <__S as Serializer>::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementationsยง

Blanket Implementationsยง

Sourceยง

impl<T> Any for T
where T: 'static + ?Sized,

Sourceยง

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
ยง

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

ยง

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

ยง

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

ยง

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Sourceยง

impl<T> Borrow<T> for T
where T: ?Sized,

Sourceยง

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Sourceยง

impl<T> BorrowMut<T> for T
where T: ?Sized,

Sourceยง

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
ยง

impl<T> Classify for T

ยง

type Classified = T

ยง

fn classify(self) -> T

ยง

impl<T> Classify for T

ยง

type Classified = T

ยง

fn classify(self) -> T

Sourceยง

impl<T> CloneToUninit for T
where T: Clone,

Sourceยง

unsafe fn clone_to_uninit(&self, dest: *mut u8)

๐Ÿ”ฌThis is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
ยง

impl<T> Declassify for T

ยง

type Declassified = T

ยง

fn declassify(self) -> T

ยง

impl<T> Declassify for T

ยง

type Declassified = T

ยง

fn declassify(self) -> T

Sourceยง

impl<T> From<T> for T

Sourceยง

fn from(t: T) -> T

Returns the argument unchanged.

ยง

impl<T> FutureExt for T

ยง

fn with_context(self, otel_cx: Context) -> WithContext<Self>

Attaches the provided Context to this type, returning a WithContext wrapper. Read more
ยง

fn with_current_context(self) -> WithContext<Self>

Attaches the current Context to this type, returning a WithContext wrapper. Read more
ยง

impl<T> Instrument for T

ยง

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
ยง

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Sourceยง

impl<T, U> Into<U> for T
where U: From<T>,

Sourceยง

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Sourceยง

impl<T> Same for T

Sourceยง

type Output = T

Should always be Self
Sourceยง

impl<T> ToOwned for T
where T: Clone,

Sourceยง

type Owned = T

The resulting type after obtaining ownership.
Sourceยง

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Sourceยง

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Sourceยง

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Sourceยง

type Error = Infallible

The type returned in the event of a conversion error.
Sourceยง

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Sourceยง

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Sourceยง

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Sourceยง

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
ยง

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

ยง

fn vzip(self) -> V

ยง

impl<T> WithSubscriber for T

ยง

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
ยง

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more
ยง

impl<T> DartSafe for T

Sourceยง

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

ยง

impl<T> TaskRetFutTrait for T
where T: Send,