Skip to main content

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