Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/openmls/openmls/llms.txt

Use this file to discover all available pages before exploring further.

StagedCommit represents a commit that has been validated but not yet merged into the group state. This allows inspection of proposed changes before applying them.

Overview

When processing a commit (either created locally or received from another member), OpenMLS creates a StagedCommit that contains:
  • All proposals covered by the commit
  • The provisional group state after the commit
  • Credentials that need verification
  • Epoch secrets for the new epoch
You can inspect the staged commit before merging it with merge_staged_commit().

Accessing staged commits

From processing messages

let processed = group.process_message(provider, message)?;

if let ProcessedMessageContent::StagedCommitMessage(staged_commit) = processed.into_content() {
    // Inspect the commit
    for proposal in staged_commit.queued_proposals() {
        println!("Proposal: {:?}", proposal.proposal());
    }
    
    // Merge when ready
    group.merge_staged_commit(provider, *staged_commit)?;
}

From pending commits

if let Some(staged_commit) = group.pending_commit() {
    // Inspect the pending commit
    let epoch = staged_commit.epoch();
}

Proposal inspection

queued_proposals

Returns an iterator over all proposals covered by the commit.
Iterator
impl Iterator<Item = &QueuedProposal>
Iterator over all queued proposals
for proposal in staged_commit.queued_proposals() {
    match proposal.proposal() {
        Proposal::Add(add) => println!("Adding member"),
        Proposal::Remove(remove) => println!("Removing {}", remove.removed.u32()),
        Proposal::Update(_) => println!("Member update"),
        _ => {}
    }
}

add_proposals

Returns an iterator over Add proposals in the commit.
Iterator
impl Iterator<Item = QueuedAddProposal>
Iterator over Add proposals
for add_proposal in staged_commit.add_proposals() {
    let key_package = add_proposal.add_proposal().key_package();
    println!("Adding: {:?}", key_package.leaf_node().credential());
}

remove_proposals

Returns an iterator over Remove proposals.
Iterator
impl Iterator<Item = QueuedRemoveProposal>
Iterator over Remove proposals
for remove_proposal in staged_commit.remove_proposals() {
    let removed_index = remove_proposal.remove_proposal().removed();
    println!("Removing member at index {}", removed_index.u32());
}

update_proposals

Returns an iterator over Update proposals.
Iterator
impl Iterator<Item = QueuedUpdateProposal>
Iterator over Update proposals

psk_proposals

Returns an iterator over PreSharedKey proposals.
Iterator
impl Iterator<Item = QueuedPskProposal>
Iterator over PSK proposals

Group state inspection

epoch

Returns the epoch that this commit transitions the group into.
GroupEpoch
GroupEpoch
The new epoch number
let new_epoch = staged_commit.epoch();
println!("Moving to epoch {}", new_epoch.as_u64());

group_context

Returns the group context for the new epoch.
GroupContext
&GroupContext
Reference to the new group context
let context = staged_commit.group_context();
println!("New tree hash: {:x?}", context.tree_hash());

update_path_leaf_node

Returns the leaf node from the update path if present.
Option
Option<&LeafNode>
The leaf node from the commit’s update path, or None if no path was included
if let Some(leaf_node) = staged_commit.update_path_leaf_node() {
    println!("Committer credential: {:?}", leaf_node.credential());
}

self_removed

Returns whether the member was removed by this commit.
bool
bool
true if the member was removed, false otherwise
if staged_commit.self_removed() {
    println!("We were removed from the group");
    // Group will transition to Inactive state after merge
}

Credential validation

credentials_to_verify

Returns credentials that the application should verify before merging the commit.
Iterator
impl Iterator<Item = &Credential>
Iterator over credentials requiring verification
for credential in staged_commit.credentials_to_verify() {
    if !credential_is_valid(credential) {
        return Err("Invalid credential in commit");
    }
}

// All credentials valid, safe to merge
group.merge_staged_commit(provider, staged_commit)?;
Credentials to verify include:
  • Credentials from Add proposals (new members)
  • Credentials from Update proposals (member updates)
  • Credentials from the update path leaf node (committer)
  • Credentials from external senders in GroupContextExtensions proposals

Secrets and export

export_secret

Exports a secret from the new epoch (before merging the commit).
crypto
&CryptoProvider
Cryptographic provider
label
&str
Label for the exported secret
context
&[u8]
Context bytes for derivation
key_length
usize
Desired key length in bytes (max: u16::MAX)
Result
Result<Vec<u8>, ExportSecretError>
Returns the exported secret, or an error if the member was removed or key length is invalid
let secret = staged_commit.export_secret(
    provider.crypto(),
    "app-secret",
    b"context",
    32,
)?;
Returns an error with UseAfterEviction if the commit removed the member from the group.

epoch_authenticator

Returns the epoch authenticator for the new epoch.
Option
Option<&EpochAuthenticator>
The epoch authenticator, or None if the member was removed
if let Some(auth) = staged_commit.epoch_authenticator() {
    // Use authenticator
}

resumption_psk_secret

Returns the resumption PSK secret for the new epoch.
Option
Option<&ResumptionPskSecret>
The resumption PSK, or None if the member was removed

Ratchet tree export

export_ratchet_tree

Exports the ratchet tree after applying this commit.
crypto
&impl OpenMlsCrypto
Cryptographic provider
original_tree
RatchetTree
The original ratchet tree before the commit
Result
Result<Option<RatchetTree>, TreeSyncFromNodesError>
Returns the new tree if the member remains in the group, None if removed
let tree = group.export_ratchet_tree();
if let Some(new_tree) = staged_commit.export_ratchet_tree(provider.crypto(), tree)? {
    // Export new tree
}

Validation workflow

Typical workflow for validating and merging a commit:
let processed = group.process_message(provider, message)?;

if let ProcessedMessageContent::StagedCommitMessage(staged_commit) = processed.into_content() {
    // 1. Check if we were removed
    if staged_commit.self_removed() {
        println!("We were removed from the group");
        group.merge_staged_commit(provider, *staged_commit)?;
        return Ok(()); // Group is now inactive
    }
    
    // 2. Validate credentials
    for credential in staged_commit.credentials_to_verify() {
        if !is_valid_credential(credential) {
            return Err("Invalid credential");
        }
    }
    
    // 3. Check proposal semantics
    for proposal in staged_commit.queued_proposals() {
        match proposal.proposal() {
            Proposal::Remove(remove) => {
                if !can_remove_member(remove.removed) {
                    return Err("Unauthorized removal");
                }
            }
            Proposal::Add(add) => {
                if group.members().count() >= MAX_MEMBERS {
                    return Err("Group full");
                }
            }
            _ => {}
        }
    }
    
    // 4. Export any needed secrets
    let app_secret = staged_commit.export_secret(
        provider.crypto(),
        "app-level-key",
        b"",
        32,
    )?;
    
    // 5. Merge the commit
    group.merge_staged_commit(provider, *staged_commit)?;
}

Analyzing membership changes

let mut added = Vec::new();
let mut removed = Vec::new();
let mut updated = Vec::new();

for proposal in staged_commit.queued_proposals() {
    match proposal.proposal() {
        Proposal::Add(add) => {
            added.push(add.key_package().leaf_node().credential().clone());
        }
        Proposal::Remove(remove) => {
            if let Some(member) = group.member(remove.removed()) {
                removed.push(member.clone());
            }
        }
        Proposal::Update(update) => {
            updated.push(update.leaf_node().credential().clone());
        }
        _ => {}
    }
}

println!("Changes: +{} -{} ~{}", added.len(), removed.len(), updated.len());

Error handling

Merging failures

match group.merge_staged_commit(provider, staged_commit) {
    Ok(()) => {
        println!("Commit merged successfully");
    }
    Err(MergeCommitError::StorageError(e)) => {
        // Handle storage error
    }
    Err(e) => {
        // Other errors
    }
}

Use cases

Access control enforcement

for proposal in staged_commit.queued_proposals() {
    if let Proposal::Remove(remove) = proposal.proposal() {
        let remover = proposal.sender();
        if !has_remove_permission(remover, remove.removed) {
            return Err("Unauthorized removal attempt");
        }
    }
}

group.merge_staged_commit(provider, staged_commit)?;

Member limit enforcement

let current_members = group.members().count();
let adds = staged_commit.add_proposals().count();
let removes = staged_commit.remove_proposals().count();

let new_member_count = current_members + adds - removes;
if new_member_count > MAX_GROUP_SIZE {
    return Err("Group size limit exceeded");
}

group.merge_staged_commit(provider, staged_commit)?;

Audit logging

let epoch = staged_commit.epoch();

for proposal in staged_commit.queued_proposals() {
    match proposal.proposal() {
        Proposal::Add(add) => {
            audit_log.record(
                epoch,
                "member_added",
                add.key_package().leaf_node().credential(),
            );
        }
        Proposal::Remove(remove) => {
            audit_log.record(epoch, "member_removed", remove.removed);
        }
        _ => {}
    }
}

group.merge_staged_commit(provider, staged_commit)?;

State transitions

After merging a StagedCommit:
  • The group advances to the new epoch
  • Pending proposals are cleared
  • Message secrets are rotated
  • If self_removed() is true, the group becomes inactive
  • Any pending commit is cleared