openmls/group/mls_group/
proposal_store.rs

1use std::collections::{hash_map::Entry, HashMap, HashSet};
2
3use openmls_traits::crypto::OpenMlsCrypto;
4use openmls_traits::types::Ciphersuite;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    binary_tree::array_representation::LeafNodeIndex,
9    ciphersuite::hash_ref::ProposalRef,
10    error::LibraryError,
11    framing::{mls_auth_content::AuthenticatedContent, mls_content::FramedContentBody, Sender},
12    group::errors::*,
13    messages::proposals::{
14        AddProposal, PreSharedKeyProposal, Proposal, ProposalOrRef, ProposalOrRefType,
15        ProposalType, RemoveProposal, UpdateProposal,
16    },
17    utils::vector_converter,
18};
19
20#[cfg(feature = "extensions-draft-08")]
21use crate::messages::proposals::AppEphemeralProposal;
22
23#[derive(Debug, Clone)]
24pub(crate) struct SelfRemoveInStore {
25    pub(crate) sender: LeafNodeIndex,
26    pub(crate) proposal_ref: ProposalRef,
27}
28
29/// A [ProposalStore] can store the standalone proposals that are received from
30/// the DS in between two commit messages.
31#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
32#[cfg_attr(any(test, feature = "test-utils"), derive(Clone))]
33pub struct ProposalStore {
34    queued_proposals: Vec<QueuedProposal>,
35}
36
37impl ProposalStore {
38    /// Create a new [`ProposalStore`].
39    pub fn new() -> Self {
40        Self {
41            queued_proposals: Vec::new(),
42        }
43    }
44    #[cfg(test)]
45    pub(crate) fn from_queued_proposal(queued_proposal: QueuedProposal) -> Self {
46        Self {
47            queued_proposals: vec![queued_proposal],
48        }
49    }
50    pub(crate) fn add(&mut self, queued_proposal: QueuedProposal) {
51        self.queued_proposals.push(queued_proposal);
52    }
53    pub(crate) fn proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
54        self.queued_proposals.iter()
55    }
56    pub(crate) fn is_empty(&self) -> bool {
57        self.queued_proposals.is_empty()
58    }
59    pub(crate) fn empty(&mut self) {
60        self.queued_proposals.clear();
61    }
62
63    /// Removes a proposal from the store using its reference. It will return
64    /// None if it wasn't found in the store.
65    pub(crate) fn remove(&mut self, proposal_ref: &ProposalRef) -> Option<()> {
66        let index = self
67            .queued_proposals
68            .iter()
69            .position(|p| &p.proposal_reference() == proposal_ref)?;
70        self.queued_proposals.remove(index);
71        Some(())
72    }
73
74    pub(crate) fn self_removes(&self) -> Vec<SelfRemoveInStore> {
75        self.queued_proposals
76            .iter()
77            .filter_map(|queued_proposal| {
78                match (queued_proposal.proposal(), queued_proposal.sender()) {
79                    (Proposal::SelfRemove, Sender::Member(sender_index)) => {
80                        Some(SelfRemoveInStore {
81                            sender: *sender_index,
82                            proposal_ref: queued_proposal.proposal_reference(),
83                        })
84                    }
85                    _ => None,
86                }
87            })
88            .collect()
89    }
90}
91
92/// Alternative representation of a Proposal, where the sender is extracted from
93/// the encapsulating PublicMessage and the ProposalRef is attached.
94#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
95pub struct QueuedProposal {
96    proposal: Proposal,
97    proposal_reference: ProposalRef,
98    sender: Sender,
99    proposal_or_ref_type: ProposalOrRefType,
100}
101
102impl QueuedProposal {
103    /// Creates a new [QueuedProposal] from an [PublicMessage]
104    pub(crate) fn from_authenticated_content_by_ref(
105        ciphersuite: Ciphersuite,
106        crypto: &impl OpenMlsCrypto,
107        public_message: AuthenticatedContent,
108    ) -> Result<Self, LibraryError> {
109        Self::from_authenticated_content(
110            ciphersuite,
111            crypto,
112            public_message,
113            ProposalOrRefType::Reference,
114        )
115    }
116
117    /// Creates a new [QueuedProposal] from an [PublicMessage]
118    pub(crate) fn from_authenticated_content(
119        ciphersuite: Ciphersuite,
120        crypto: &impl OpenMlsCrypto,
121        public_message: AuthenticatedContent,
122        proposal_or_ref_type: ProposalOrRefType,
123    ) -> Result<Self, LibraryError> {
124        let proposal_reference =
125            ProposalRef::from_authenticated_content_by_ref(crypto, ciphersuite, &public_message)
126                .map_err(|_| LibraryError::custom("Could not calculate `ProposalRef`."))?;
127
128        let (body, sender) = public_message.into_body_and_sender();
129
130        let proposal = match body {
131            FramedContentBody::Proposal(p) => p,
132            _ => return Err(LibraryError::custom("Wrong content type")),
133        };
134
135        Ok(Self {
136            proposal,
137            proposal_reference,
138            sender,
139            proposal_or_ref_type,
140        })
141    }
142
143    /// Creates a new [QueuedProposal] from a [Proposal] and [Sender]
144    ///
145    /// Note: We should calculate the proposal ref by hashing the authenticated
146    /// content but can't do this here without major refactoring. Thus, we
147    /// use an internal `from_raw_proposal` hash.
148    pub(crate) fn from_proposal_and_sender(
149        ciphersuite: Ciphersuite,
150        crypto: &impl OpenMlsCrypto,
151        proposal: Proposal,
152        sender: &Sender,
153    ) -> Result<Self, LibraryError> {
154        let proposal_reference = ProposalRef::from_raw_proposal(ciphersuite, crypto, &proposal)?;
155        Ok(Self {
156            proposal,
157            proposal_reference,
158            sender: sender.clone(),
159            proposal_or_ref_type: ProposalOrRefType::Proposal,
160        })
161    }
162
163    /// Returns the `Proposal` as a reference
164    pub fn proposal(&self) -> &Proposal {
165        &self.proposal
166    }
167    /// Returns the `ProposalRef`.
168    pub(crate) fn proposal_reference(&self) -> ProposalRef {
169        self.proposal_reference.clone()
170    }
171
172    /// Returns the `ProposalRef`.
173    pub(crate) fn proposal_reference_ref(&self) -> &ProposalRef {
174        &self.proposal_reference
175    }
176
177    /// Returns the `ProposalOrRefType`.
178    pub fn proposal_or_ref_type(&self) -> ProposalOrRefType {
179        self.proposal_or_ref_type
180    }
181    /// Returns the `Sender` as a reference
182    pub fn sender(&self) -> &Sender {
183        &self.sender
184    }
185}
186
187/// Helper struct to collect proposals such that they are unique and can be read
188/// out in the order in that they were added.
189struct OrderedProposalRefs {
190    proposal_refs: HashSet<ProposalRef>,
191    ordered_proposal_refs: Vec<ProposalRef>,
192}
193
194impl OrderedProposalRefs {
195    fn new() -> Self {
196        Self {
197            proposal_refs: HashSet::new(),
198            ordered_proposal_refs: Vec::new(),
199        }
200    }
201
202    /// Adds a proposal reference to the queue. If the proposal reference is
203    /// already in the queue, it ignores it.
204    fn add(&mut self, proposal_ref: ProposalRef) {
205        // The `insert` function of the `HashSet` returns `true` if the element
206        // is new to the set.
207        if self.proposal_refs.insert(proposal_ref.clone()) {
208            self.ordered_proposal_refs.push(proposal_ref);
209        }
210    }
211
212    /// Returns an iterator over the proposal references in the order in which
213    /// they were inserted.
214    fn iter(&self) -> impl Iterator<Item = &ProposalRef> {
215        self.ordered_proposal_refs.iter()
216    }
217}
218
219/// Proposal queue that helps filtering and sorting Proposals received during
220/// one epoch. The Proposals are stored in a `HashMap` which maps Proposal
221/// references to Proposals, such that, given a reference, a proposal can be
222/// accessed efficiently. To enable iteration over the queue in order, the
223/// `ProposalQueue` also contains a vector of `ProposalRef`s.
224#[derive(Default, Debug, Serialize, Deserialize)]
225#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
226pub struct ProposalQueue {
227    /// `proposal_references` holds references to the proposals in the queue and
228    /// determines the order of the queue.
229    proposal_references: Vec<ProposalRef>,
230    /// `queued_proposals` contains the actual proposals in the queue. They are
231    /// stored in a `HashMap` to allow for efficient access to the proposals.
232    #[serde(with = "vector_converter")]
233    queued_proposals: HashMap<ProposalRef, QueuedProposal>,
234}
235
236impl ProposalQueue {
237    /// Returns `true` if the [`ProposalQueue`] is empty. Otherwise returns
238    /// `false`.
239    pub(crate) fn is_empty(&self) -> bool {
240        self.proposal_references.is_empty()
241    }
242
243    /// Returns a new `QueuedProposalQueue` from proposals that were committed
244    /// and don't need filtering.
245    /// This functions does the following checks:
246    ///  - ValSem200
247    pub(crate) fn from_committed_proposals(
248        ciphersuite: Ciphersuite,
249        crypto: &impl OpenMlsCrypto,
250        committed_proposals: Vec<ProposalOrRef>,
251        proposal_store: &ProposalStore,
252        sender: &Sender,
253    ) -> Result<Self, FromCommittedProposalsError> {
254        log::debug!("from_committed_proposals");
255        // Feed the `proposals_by_reference` in a `HashMap` so that we can easily
256        // extract then by reference later
257        let mut proposals_by_reference_queue: HashMap<ProposalRef, QueuedProposal> = HashMap::new();
258        for queued_proposal in proposal_store.proposals() {
259            proposals_by_reference_queue.insert(
260                queued_proposal.proposal_reference(),
261                queued_proposal.clone(),
262            );
263        }
264        log::trace!("   known proposals:\n{proposals_by_reference_queue:#?}");
265        // Build the actual queue
266        let mut proposal_queue = ProposalQueue::default();
267
268        // Iterate over the committed proposals and insert the proposals in the queue
269        log::trace!("   committed proposals ...");
270        for proposal_or_ref in committed_proposals.into_iter() {
271            log::trace!("       proposal_or_ref:\n{proposal_or_ref:#?}");
272            let queued_proposal = match proposal_or_ref {
273                ProposalOrRef::Proposal(proposal) => {
274                    // ValSem200
275                    if proposal
276                        .as_remove()
277                        .and_then(|remove_proposal| {
278                            sender.as_member().filter(|leaf_index| {
279                                // The proposal must not remove the committer.
280                                remove_proposal.removed() == *leaf_index
281                            })
282                        })
283                        .is_some()
284                    {
285                        return Err(FromCommittedProposalsError::SelfRemoval);
286                    };
287
288                    QueuedProposal::from_proposal_and_sender(
289                        ciphersuite,
290                        crypto,
291                        *proposal,
292                        sender,
293                    )?
294                }
295                ProposalOrRef::Reference(ref proposal_reference) => {
296                    match proposals_by_reference_queue.get(proposal_reference) {
297                        Some(queued_proposal) => {
298                            // ValSem200
299                            if let Proposal::Remove(ref remove_proposal) = queued_proposal.proposal
300                            {
301                                if let Sender::Member(leaf_index) = sender {
302                                    if remove_proposal.removed() == *leaf_index {
303                                        return Err(FromCommittedProposalsError::SelfRemoval);
304                                    }
305                                }
306                            }
307
308                            queued_proposal.clone()
309                        }
310                        None => return Err(FromCommittedProposalsError::ProposalNotFound),
311                    }
312                }
313            };
314            proposal_queue.add(queued_proposal);
315        }
316
317        Ok(proposal_queue)
318    }
319
320    /// Returns proposal for a given proposal ID
321    pub fn get(&self, proposal_reference: &ProposalRef) -> Option<&QueuedProposal> {
322        self.queued_proposals.get(proposal_reference)
323    }
324
325    /// Add a new [QueuedProposal] to the queue
326    pub(crate) fn add(&mut self, queued_proposal: QueuedProposal) {
327        let proposal_reference = queued_proposal.proposal_reference();
328        // Only add the proposal if it's not already there
329        if let Entry::Vacant(entry) = self.queued_proposals.entry(proposal_reference.clone()) {
330            // Add the proposal reference to ensure the correct order
331            self.proposal_references.push(proposal_reference);
332            // Add the proposal to the queue
333            entry.insert(queued_proposal);
334        }
335    }
336
337    /// Returns an iterator over a list of `QueuedProposal` filtered by proposal
338    /// type
339    pub(crate) fn filtered_by_type(
340        &self,
341        proposal_type: ProposalType,
342    ) -> impl Iterator<Item = &QueuedProposal> {
343        // Iterate over the reference to extract the proposals in the right order
344        self.proposal_references
345            .iter()
346            .filter(move |&pr| match self.queued_proposals.get(pr) {
347                Some(p) => p.proposal.is_type(proposal_type),
348                None => false,
349            })
350            .filter_map(move |reference| self.get(reference))
351    }
352
353    /// Returns an iterator over all `QueuedProposal` in the queue
354    /// in the order of the the Commit message
355    pub(crate) fn queued_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
356        // Iterate over the reference to extract the proposals in the right order
357        self.proposal_references
358            .iter()
359            .filter_map(move |reference| self.get(reference))
360    }
361
362    /// Returns an iterator over all Add proposals in the queue
363    /// in the order of the the Commit message
364    pub(crate) fn add_proposals(&self) -> impl Iterator<Item = QueuedAddProposal<'_>> {
365        self.queued_proposals().filter_map(|queued_proposal| {
366            if let Proposal::Add(add_proposal) = queued_proposal.proposal() {
367                let sender = queued_proposal.sender();
368                Some(QueuedAddProposal {
369                    add_proposal,
370                    sender,
371                })
372            } else {
373                None
374            }
375        })
376    }
377
378    /// Returns an iterator over all Remove proposals in the queue
379    /// in the order of the the Commit message
380    pub(crate) fn remove_proposals(&self) -> impl Iterator<Item = QueuedRemoveProposal<'_>> {
381        self.queued_proposals().filter_map(|queued_proposal| {
382            if let Proposal::Remove(remove_proposal) = queued_proposal.proposal() {
383                let sender = queued_proposal.sender();
384                Some(QueuedRemoveProposal {
385                    remove_proposal,
386                    sender,
387                })
388            } else {
389                None
390            }
391        })
392    }
393
394    /// Returns an iterator over all Update in the queue
395    /// in the order of the the Commit message
396    pub(crate) fn update_proposals(&self) -> impl Iterator<Item = QueuedUpdateProposal<'_>> {
397        self.queued_proposals().filter_map(|queued_proposal| {
398            if let Proposal::Update(update_proposal) = queued_proposal.proposal() {
399                let sender = queued_proposal.sender();
400                Some(QueuedUpdateProposal {
401                    update_proposal,
402                    sender,
403                })
404            } else {
405                None
406            }
407        })
408    }
409
410    /// Returns an iterator over all PresharedKey proposals in the queue
411    /// in the order of the the Commit message
412    pub(crate) fn psk_proposals(&self) -> impl Iterator<Item = QueuedPskProposal<'_>> {
413        self.queued_proposals().filter_map(|queued_proposal| {
414            if let Proposal::PreSharedKey(psk_proposal) = queued_proposal.proposal() {
415                let sender = queued_proposal.sender();
416                Some(QueuedPskProposal {
417                    psk_proposal,
418                    sender,
419                })
420            } else {
421                None
422            }
423        })
424    }
425
426    #[cfg(feature = "extensions-draft-08")]
427    /// Returns an iterator over all AppEphemeral proposals in the queue
428    /// in the order of the Commit message
429    pub(crate) fn app_ephemeral_proposals(
430        &self,
431    ) -> impl Iterator<Item = QueuedAppEphemeralProposal<'_>> {
432        self.queued_proposals().filter_map(|queued_proposal| {
433            if let Proposal::AppEphemeral(app_ephemeral_proposal) = queued_proposal.proposal() {
434                let sender = queued_proposal.sender();
435                Some(QueuedAppEphemeralProposal {
436                    app_ephemeral_proposal,
437                    sender,
438                })
439            } else {
440                None
441            }
442        })
443    }
444
445    /// Filters received proposals
446    ///
447    /// 11.2 Commit
448    /// If there are multiple proposals that apply to the same leaf,
449    /// the committer chooses one and includes only that one in the Commit,
450    /// considering the rest invalid. The committer MUST prefer any Remove
451    /// received, or the most recent Update for the leaf if there are no
452    /// Removes. If there are multiple Add proposals for the same client,
453    /// the committer again chooses one to include and considers the rest
454    /// invalid.
455    ///
456    /// The function performs the following steps:
457    ///
458    /// - Extract Adds and filter for duplicates
459    /// - Build member list with chains: Updates, Removes & SelfRemoves
460    /// - Check for invalid indexes and drop proposal
461    /// - Check for presence of SelfRemoves and delete Removes and Updates
462    /// - Check for presence of Removes and delete Updates
463    /// - Only keep the last Update
464    ///
465    /// Return a [`ProposalQueue`] and a bool that indicates whether Updates for
466    /// the own node were included
467    pub(crate) fn filter_proposals(
468        iter: impl IntoIterator<Item = QueuedProposal>,
469        own_index: LeafNodeIndex,
470    ) -> Result<(Self, bool), ProposalQueueError> {
471        // We use a HashSet to filter out duplicate Adds and use a vector in
472        // addition to keep the order as they come in.
473        let mut adds: OrderedProposalRefs = OrderedProposalRefs::new();
474        let mut valid_proposals: OrderedProposalRefs = OrderedProposalRefs::new();
475        let mut proposal_pool: HashMap<ProposalRef, QueuedProposal> = HashMap::new();
476        let mut contains_own_updates = false;
477        let mut contains_external_init = false;
478
479        let mut member_specific_proposals: HashMap<LeafNodeIndex, QueuedProposal> = HashMap::new();
480        let mut register_member_specific_proposal =
481            |member: LeafNodeIndex, proposal: QueuedProposal| {
482                // Only replace if the existing proposal is an Update.
483                match member_specific_proposals.entry(member) {
484                    // Insert if no entry exists for this sender.
485                    Entry::Vacant(vacant_entry) => {
486                        vacant_entry.insert(proposal);
487                    }
488                    // Replace the existing proposal if the new proposal has
489                    // priority.
490                    Entry::Occupied(mut occupied_entry)
491                        if occupied_entry
492                            .get()
493                            .proposal()
494                            .has_lower_priority_than(&proposal.proposal) =>
495                    {
496                        occupied_entry.insert(proposal);
497                    }
498                    // Otherwise ignore the new proposal.
499                    Entry::Occupied(_) => {}
500                }
501            };
502
503        // Parse proposals and build adds and member list
504        for queued_proposal in iter {
505            proposal_pool.insert(
506                queued_proposal.proposal_reference(),
507                queued_proposal.clone(),
508            );
509            match queued_proposal.proposal {
510                Proposal::Add(_) => {
511                    adds.add(queued_proposal.proposal_reference());
512                }
513                Proposal::Update(_) => {
514                    // Only members can send update proposals
515                    // ValSem112
516                    let Sender::Member(sender_index) = queued_proposal.sender() else {
517                        return Err(ProposalQueueError::UpdateFromExternalSender);
518                    };
519                    if sender_index == &own_index {
520                        contains_own_updates = true;
521                        continue;
522                    }
523                    register_member_specific_proposal(*sender_index, queued_proposal);
524                }
525                Proposal::Remove(ref remove_proposal) => {
526                    let removed = remove_proposal.removed();
527                    register_member_specific_proposal(removed, queued_proposal);
528                }
529                Proposal::PreSharedKey(_) => {
530                    valid_proposals.add(queued_proposal.proposal_reference());
531                }
532                Proposal::ReInit(_) => {
533                    // TODO #751: Only keep one ReInit
534                }
535                Proposal::ExternalInit(_) => {
536                    // Only use the first external init proposal we find.
537                    if !contains_external_init {
538                        valid_proposals.add(queued_proposal.proposal_reference());
539                        contains_external_init = true;
540                    }
541                }
542                Proposal::GroupContextExtensions(_) => {
543                    valid_proposals.add(queued_proposal.proposal_reference());
544                }
545                Proposal::SelfRemove => {
546                    let Sender::Member(removed) = queued_proposal.sender() else {
547                        return Err(ProposalQueueError::SelfRemoveFromNonMember);
548                    };
549                    register_member_specific_proposal(*removed, queued_proposal);
550                }
551                #[cfg(feature = "extensions-draft-08")]
552                Proposal::AppEphemeral(_) => {
553                    valid_proposals.add(queued_proposal.proposal_reference());
554                }
555                Proposal::Custom(_) => {
556                    // Other/unknown proposals are always considered valid and
557                    // have to be checked by the application instead.
558                    valid_proposals.add(queued_proposal.proposal_reference());
559                }
560            }
561        }
562
563        // Add the leaf-specific proposals to the list of valid proposals.
564        for proposal in member_specific_proposals.values() {
565            valid_proposals.add(proposal.proposal_reference());
566        }
567
568        // Only retain `adds` and `valid_proposals`
569        let mut proposal_queue = ProposalQueue::default();
570        for proposal_reference in adds.iter().chain(valid_proposals.iter()) {
571            let queued_proposal = proposal_pool
572                .get(proposal_reference)
573                .cloned()
574                .ok_or(ProposalQueueError::ProposalNotFound)?;
575            proposal_queue.add(queued_proposal);
576        }
577        Ok((proposal_queue, contains_own_updates))
578    }
579
580    /// Returns `true` if all `ProposalRef` values from the list are
581    /// contained in the queue
582    #[cfg(test)]
583    pub(crate) fn contains(&self, proposal_reference_list: &[ProposalRef]) -> bool {
584        for proposal_reference in proposal_reference_list {
585            if !self.queued_proposals.contains_key(proposal_reference) {
586                return false;
587            }
588        }
589        true
590    }
591
592    /// Returns the list of all proposals that are covered by a Commit
593    pub(crate) fn commit_list(&self) -> Vec<ProposalOrRef> {
594        // Iterate over the reference to extract the proposals in the right order
595        self.proposal_references
596            .iter()
597            .filter_map(|proposal_reference| self.queued_proposals.get(proposal_reference))
598            .map(|queued_proposal| {
599                // Differentiate the type of proposal
600                match queued_proposal.proposal_or_ref_type {
601                    ProposalOrRefType::Proposal => {
602                        ProposalOrRef::proposal(queued_proposal.proposal.clone())
603                    }
604                    ProposalOrRefType::Reference => {
605                        ProposalOrRef::reference(queued_proposal.proposal_reference.clone())
606                    }
607                }
608            })
609            .collect::<Vec<ProposalOrRef>>()
610    }
611}
612
613impl Extend<QueuedProposal> for ProposalQueue {
614    fn extend<T: IntoIterator<Item = QueuedProposal>>(&mut self, iter: T) {
615        for proposal in iter {
616            self.add(proposal)
617        }
618    }
619}
620
621impl IntoIterator for ProposalQueue {
622    type Item = QueuedProposal;
623
624    type IntoIter = std::collections::hash_map::IntoValues<ProposalRef, QueuedProposal>;
625
626    fn into_iter(self) -> Self::IntoIter {
627        self.queued_proposals.into_values()
628    }
629}
630
631impl<'a> IntoIterator for &'a ProposalQueue {
632    type Item = &'a QueuedProposal;
633
634    type IntoIter = std::collections::hash_map::Values<'a, ProposalRef, QueuedProposal>;
635
636    fn into_iter(self) -> Self::IntoIter {
637        self.queued_proposals.values()
638    }
639}
640
641impl FromIterator<QueuedProposal> for ProposalQueue {
642    fn from_iter<T: IntoIterator<Item = QueuedProposal>>(iter: T) -> Self {
643        let mut out = Self::default();
644        out.extend(iter);
645        out
646    }
647}
648
649/// A queued Add proposal
650#[derive(PartialEq, Debug)]
651pub struct QueuedAddProposal<'a> {
652    add_proposal: &'a AddProposal,
653    sender: &'a Sender,
654}
655
656impl QueuedAddProposal<'_> {
657    /// Returns a reference to the proposal
658    pub fn add_proposal(&self) -> &AddProposal {
659        self.add_proposal
660    }
661
662    /// Returns a reference to the sender
663    pub fn sender(&self) -> &Sender {
664        self.sender
665    }
666}
667
668/// A queued Remove proposal
669#[derive(PartialEq, Eq, Debug)]
670pub struct QueuedRemoveProposal<'a> {
671    remove_proposal: &'a RemoveProposal,
672    sender: &'a Sender,
673}
674
675impl QueuedRemoveProposal<'_> {
676    /// Returns a reference to the proposal
677    pub fn remove_proposal(&self) -> &RemoveProposal {
678        self.remove_proposal
679    }
680
681    /// Returns a reference to the sender
682    pub fn sender(&self) -> &Sender {
683        self.sender
684    }
685}
686
687/// A queued Update proposal
688#[derive(PartialEq, Eq, Debug)]
689pub struct QueuedUpdateProposal<'a> {
690    update_proposal: &'a UpdateProposal,
691    sender: &'a Sender,
692}
693
694impl QueuedUpdateProposal<'_> {
695    /// Returns a reference to the proposal
696    pub fn update_proposal(&self) -> &UpdateProposal {
697        self.update_proposal
698    }
699
700    /// Returns a reference to the sender
701    pub fn sender(&self) -> &Sender {
702        self.sender
703    }
704}
705
706/// A queued PresharedKey proposal
707#[derive(PartialEq, Eq, Debug)]
708pub struct QueuedPskProposal<'a> {
709    psk_proposal: &'a PreSharedKeyProposal,
710    sender: &'a Sender,
711}
712
713impl QueuedPskProposal<'_> {
714    /// Returns a reference to the proposal
715    pub fn psk_proposal(&self) -> &PreSharedKeyProposal {
716        self.psk_proposal
717    }
718
719    /// Returns a reference to the sender
720    pub fn sender(&self) -> &Sender {
721        self.sender
722    }
723}
724
725#[cfg(feature = "extensions-draft-08")]
726/// A queued Add proposal
727#[derive(PartialEq, Debug)]
728pub struct QueuedAppEphemeralProposal<'a> {
729    app_ephemeral_proposal: &'a AppEphemeralProposal,
730    sender: &'a Sender,
731}
732
733#[cfg(feature = "extensions-draft-08")]
734impl QueuedAppEphemeralProposal<'_> {
735    /// Returns a reference to the proposal
736    pub fn app_ephemeral_proposal(&self) -> &AppEphemeralProposal {
737        self.app_ephemeral_proposal
738    }
739
740    /// Returns a reference to the sender
741    pub fn sender(&self) -> &Sender {
742        self.sender
743    }
744}