use gbp_stack::{
ControlOpcode, GroupNode, GtpAccept, GtpClient, MlsContext, NodeState, ProcessedKind,
};
use openmls::prelude::DeserializeBytes as _;
use openmls::prelude::{KeyPackage, KeyPackageIn, ProtocolVersion};
use openmls_traits::OpenMlsProvider as _;
fn validated_kp(ctx: &MlsContext, raw: &[u8]) -> KeyPackage {
let kp_in = KeyPackageIn::tls_deserialize_exact_bytes(raw).expect("kp parse");
kp_in
.validate(ctx.provider.crypto(), ProtocolVersion::Mls10)
.expect("kp validate")
}
fn export_kp(bundle: &openmls::prelude::KeyPackageBundle) -> Vec<u8> {
openmls::prelude::tls_codec::Serialize::tls_serialize_detached(bundle.key_package()).unwrap()
}
struct Member {
_name: &'static str,
mls: MlsContext,
node: GroupNode,
gtp: GtpClient,
}
impl Member {
fn new_creator(name: &'static str, member_id: u32) -> Self {
let (mls, _) = MlsContext::new_member(name.as_bytes()).unwrap();
let mut node = GroupNode::new(member_id, mls.group_id_16());
node.bootstrap_as_creator(0);
Self { _name: name, mls, node, gtp: GtpClient::new() }
}
fn new_pending(name: &'static str) -> (Self, openmls::prelude::KeyPackageBundle) {
let (mls, bundle) = MlsContext::new_member(name.as_bytes()).unwrap();
let placeholder = GroupNode::new(0, [0u8; 16]);
(
Self { _name: name, mls, node: placeholder, gtp: GtpClient::new() },
bundle,
)
}
fn finish_join(&mut self, member_id: u32, arrival_tid: u32) {
let mls_epoch = self.mls.epoch();
self.node = GroupNode::new(member_id, self.mls.group_id_16());
self.node
.bootstrap_as_joiner(mls_epoch.saturating_sub(1), 0);
self.node.apply_transition(arrival_tid);
}
}
fn first_payload(events: &[gbp_stack::Event]) -> Option<&gbp_stack::DeliveredPayload> {
events.iter().find_map(|e| match e {
gbp_stack::Event::PayloadReceived(p) => Some(p),
_ => None,
})
}
#[test]
fn full_lifecycle_two_joins_one_leave() {
let mut alice = Member::new_creator("alice", 1);
let (mut bob, bob_kp_bundle) = Member::new_pending("bob");
let kp_bytes = export_kp(&bob_kp_bundle);
let validated = validated_kp(&alice.mls, &kp_bytes);
let (_commit_bytes, welcome_bytes) = alice.mls.invite_full(&[validated]).unwrap();
assert_eq!(alice.mls.epoch(), 0, "no merge yet");
bob.mls.accept_welcome(&welcome_bytes).unwrap();
bob.finish_join(2, 1);
alice.mls.finalize_pending_commit().unwrap();
let exec1 = alice
.node
.send_control(&mut alice.mls, 0, ControlOpcode::ExecuteTransition, 1, 1, vec![])
.unwrap();
alice.node.apply_transition(1);
let _ = bob.node.on_wire(&mut bob.mls, &exec1.wire).unwrap();
assert_eq!(alice.node.last_transition_id, 1);
assert_eq!(bob.node.last_transition_id, 1);
let m = alice
.gtp
.send(&mut alice.node, &mut alice.mls, 0, 100, "hello bob")
.unwrap();
let evs = bob.node.on_wire(&mut bob.mls, &m.wire).unwrap();
let pr = first_payload(&evs).expect("bob got alice's text");
let plain = pr.plaintext.clone();
let accept = bob.gtp.accept(&plain, bob.node.current_epoch).unwrap();
match accept {
GtpAccept::New(msg) => assert_eq!(msg.text().unwrap(), "hello bob"),
other => panic!("expected New, got {:?}", other),
}
let (mut carol, carol_kp_bundle) = Member::new_pending("carol");
let kp_bytes = export_kp(&carol_kp_bundle);
let v_carol = validated_kp(&alice.mls, &kp_bytes);
let (commit2, welcome2) = alice.mls.invite_full(&[v_carol]).unwrap();
assert_eq!(alice.mls.epoch(), 1, "still pre-merge for tid=2");
let prepare2 = alice
.node
.send_control(&mut alice.mls, 0, ControlOpcode::PrepareTransition, 2, 10, commit2.clone())
.unwrap();
let bob_evs = bob.node.on_wire(&mut bob.mls, &prepare2.wire).unwrap();
let prepare_args = bob_evs.iter().find_map(|e| match e {
gbp_stack::Event::Control { opcode: ControlOpcode::PrepareTransition, args, .. } => Some(args.clone()),
_ => None,
}).expect("bob saw PREPARE");
assert_eq!(prepare_args, commit2);
let kind = bob.mls.process_message(&prepare_args).unwrap();
assert_eq!(kind, ProcessedKind::Commit);
assert_eq!(bob.mls.epoch(), 1, "deferred merge — staged but not advanced");
let exec2 = alice
.node
.send_control(&mut alice.mls, 0, ControlOpcode::ExecuteTransition, 2, 11, vec![])
.unwrap();
alice.node.apply_transition(2);
alice.mls.finalize_pending_commit().unwrap();
let _ = bob.node.on_wire(&mut bob.mls, &exec2.wire).unwrap();
bob.mls.finalize_pending_commit().unwrap();
carol.mls.accept_welcome(&welcome2).unwrap();
carol.finish_join(3, 2);
assert_eq!(alice.node.last_transition_id, 2);
assert_eq!(bob.node.last_transition_id, 2);
assert_eq!(carol.node.last_transition_id, 2);
assert_eq!(alice.node.current_epoch, 2);
assert_eq!(bob.node.current_epoch, 2);
assert_eq!(carol.node.current_epoch, 2);
let m2 = bob.gtp.send(&mut bob.node, &mut bob.mls, 0, 200, "everyone hi").unwrap();
{
let evs = alice.node.on_wire(&mut alice.mls, &m2.wire).unwrap();
let plain = first_payload(&evs).expect("alice missed bob").plaintext.clone();
let acc = alice.gtp.accept(&plain, alice.node.current_epoch).unwrap();
if let GtpAccept::New(m) = acc {
assert_eq!(m.text().unwrap(), "everyone hi");
} else {
panic!("alice: {acc:?}");
}
}
{
let evs = carol.node.on_wire(&mut carol.mls, &m2.wire).unwrap();
let plain = first_payload(&evs).expect("carol missed bob").plaintext.clone();
let acc = carol.gtp.accept(&plain, carol.node.current_epoch).unwrap();
if let GtpAccept::New(m) = acc {
assert_eq!(m.text().unwrap(), "everyone hi");
} else {
panic!("carol: {acc:?}");
}
}
let commit3 = alice.mls.remove_members(&[1]).unwrap();
assert_eq!(alice.mls.epoch(), 2, "still pre-merge for tid=3");
let prepare3 = alice
.node
.send_control(&mut alice.mls, 0, ControlOpcode::PrepareTransition, 3, 20, commit3.clone())
.unwrap();
let carol_evs = carol.node.on_wire(&mut carol.mls, &prepare3.wire).unwrap();
let p3args = carol_evs.iter().find_map(|e| match e {
gbp_stack::Event::Control { opcode: ControlOpcode::PrepareTransition, args, .. } => Some(args.clone()),
_ => None,
}).expect("carol saw PREPARE");
let kind = carol.mls.process_message(&p3args).unwrap();
assert_eq!(kind, ProcessedKind::Commit);
assert_eq!(carol.mls.epoch(), 2, "deferred merge — still on 2 until finalize");
let _ = bob.mls.process_message(&p3args);
let exec3 = alice
.node
.send_control(&mut alice.mls, 0, ControlOpcode::ExecuteTransition, 3, 21, vec![])
.unwrap();
alice.node.apply_transition(3);
alice.mls.finalize_pending_commit().unwrap();
let _ = carol.node.on_wire(&mut carol.mls, &exec3.wire).unwrap();
carol.mls.finalize_pending_commit().unwrap();
assert_eq!(alice.node.last_transition_id, 3);
assert_eq!(carol.node.last_transition_id, 3);
assert_eq!(alice.node.current_epoch, 3);
assert_eq!(carol.node.current_epoch, 3);
let m3 = carol.gtp.send(&mut carol.node, &mut carol.mls, 0, 300, "after-leave").unwrap();
let evs = alice.node.on_wire(&mut alice.mls, &m3.wire).unwrap();
let plain = first_payload(&evs).expect("alice missed carol post-leave").plaintext.clone();
let acc = alice.gtp.accept(&plain, alice.node.current_epoch).unwrap();
if let GtpAccept::New(m) = acc {
assert_eq!(m.text().unwrap(), "after-leave");
} else {
panic!("{acc:?}");
}
let bob_evs_after = bob.node.on_wire(&mut bob.mls, &m3.wire).unwrap();
let bob_err = bob_evs_after.iter().find_map(|e| match e {
gbp_stack::Event::Error { code, .. } => Some(*code),
_ => None,
});
assert!(bob_err.is_some(), "bob must observe a decryption / state error");
assert_eq!(alice.node.state, NodeState::Active);
assert_eq!(carol.node.state, NodeState::Active);
}