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