Skip to main content

macula_rust/pool/
call.rs

1//! Calls and streams that reach a provider at its own station, and the DHT
2//! through the pool's links.
3//!
4//! A call resolves the procedure's advertisements from the DHT, keeps those
5//! the realm's pinned key authorizes (or, in a node's own namespace, those
6//! that node signed), and tries the freshest first: it dials the serving
7//! station the advertisement names, pinned by its node_id from the station's
8//! own station_endpoint record, and calls the provider there. It moves on to
9//! the next candidate only when a station cannot be reached within that
10//! candidate's share of the deadline, before anything is sent: once the CALL
11//! or STREAM_OPEN has gone out, under the whole deadline, its outcome is
12//! returned as it is, a timeout included, so one call reaches a provider at
13//! most once (macula's call_work and failure_scope/1). A candidate that
14//! answered is remembered until its advertisement expires.
15
16use std::collections::HashSet;
17use std::fmt;
18use std::future::Future;
19use std::str::FromStr;
20use std::sync::Arc;
21use std::time::Duration;
22
23use tokio::time::Instant;
24
25use crate::cbor::Value;
26use crate::frame::StreamMode;
27use crate::record::{self, RecordType, Trust, Verified};
28use crate::seal::KEY_ID_SIZE;
29use crate::station_link::{self, Link, LinkError, Stream, DEFAULT_CALL_TIMEOUT};
30use crate::transport::Target;
31
32use super::{Pool, PoolError, PoolInner};
33
34/// No candidate gets less than a second of a call's time.
35const MIN_CANDIDATE_SHARE: Duration = Duration::from_secs(1);
36
37/// How a call or an open must be kept, as macula-go's options name it
38/// (macula 13, E2E design §8): `Preferred`, the default, or `Required`.
39/// There is no `off`: only an advertisement naming no key is called in the
40/// clear. This SDK seals nothing yet, so a provider whose advertisement names
41/// a KEM key is never called, and `Required` calls no provider: a lookup can
42/// deny a call, never downgrade it.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum Confidentiality {
45    /// A provider whose advertisement names no KEM key is called in the
46    /// clear.
47    #[default]
48    Preferred,
49    /// Only a sealed call, which this SDK does not make yet.
50    Required,
51}
52
53impl FromStr for Confidentiality {
54    type Err = PoolError;
55
56    /// "preferred" (or "", the default) or "required"; "off" and anything
57    /// else are refused, as macula-go's call options refuse them.
58    fn from_str(s: &str) -> Result<Self, PoolError> {
59        match s {
60            "" | "preferred" => Ok(Confidentiality::Preferred),
61            "required" => Ok(Confidentiality::Required),
62            "off" => Err(PoolError::InvalidOpts(
63                "confidential off is refused for a call or an open: it is preferred or required"
64                    .into(),
65            )),
66            other => Err(PoolError::InvalidOpts(format!(
67                "confidential is preferred or required, not {other:?}"
68            ))),
69        }
70    }
71}
72
73/// Why a call could not be kept confidential, as macula's
74/// {error, {confidentiality, Reason}} names it.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum ConfidentialityReason {
77    /// Every trusted provider names a KEM key this node cannot seal to, or
78    /// the call requires a seal and none can be made.
79    NoKemKey,
80}
81
82impl ConfidentialityReason {
83    /// The reason as macula names it.
84    pub fn name(self) -> &'static str {
85        match self {
86            ConfidentialityReason::NoKemKey => "no_kem_key",
87        }
88    }
89}
90
91/// A call or an open that could not be kept confidential, and so was not
92/// made: nothing was sent. `advertised` holds the key ids the trusted
93/// providers' advertisements named.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct ConfidentialityError {
96    pub reason: ConfidentialityReason,
97    pub advertised: Vec<[u8; KEY_ID_SIZE]>,
98}
99
100impl fmt::Display for ConfidentialityError {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "confidentiality: {}", self.reason.name())?;
103        for id in &self.advertised {
104            f.write_str(" ")?;
105            for b in id {
106                write!(f, "{b:02x}")?;
107            }
108        }
109        Ok(())
110    }
111}
112
113/// A call to a procedure: its realm and name, the provider to call (any
114/// trusted one when zero), the payload, how long to wait
115/// ([`DEFAULT_CALL_TIMEOUT`] when zero), a UCAN and its proofs for a gated
116/// procedure, and how it must be kept.
117#[derive(Debug, Clone, PartialEq)]
118pub struct Call {
119    pub realm: [u8; 32],
120    pub procedure: String,
121    pub provider: [u8; 32],
122    pub payload: Value,
123    pub timeout: Duration,
124    pub token: Option<Vec<u8>>,
125    pub proofs: Vec<Vec<u8>>,
126    pub confidential: Confidentiality,
127}
128
129impl Default for Call {
130    fn default() -> Self {
131        Call {
132            realm: [0; 32],
133            procedure: String::new(),
134            provider: [0; 32],
135            payload: Value::Map(Vec::new()),
136            timeout: Duration::ZERO,
137            token: None,
138            proofs: Vec::new(),
139            confidential: Confidentiality::Preferred,
140        }
141    }
142}
143
144/// A streaming session to open: its realm and name, the provider (any
145/// trusted one when zero), the mode, the open's payload, its deadline (the
146/// link's default when zero), a UCAN and its proofs for a gated procedure,
147/// and how it must be kept. Its default mode is server_stream.
148#[derive(Debug, Clone, PartialEq)]
149pub struct StreamCall {
150    pub realm: [u8; 32],
151    pub procedure: String,
152    pub provider: [u8; 32],
153    pub mode: StreamMode,
154    pub payload: Value,
155    pub deadline: Duration,
156    pub token: Option<Vec<u8>>,
157    pub proofs: Vec<Vec<u8>>,
158    pub confidential: Confidentiality,
159}
160
161impl Default for StreamCall {
162    fn default() -> Self {
163        StreamCall {
164            realm: [0; 32],
165            procedure: String::new(),
166            provider: [0; 32],
167            mode: StreamMode::ServerStream,
168            payload: Value::Map(Vec::new()),
169            deadline: Duration::ZERO,
170            token: None,
171            proofs: Vec::new(),
172            confidential: Confidentiality::Preferred,
173        }
174    }
175}
176
177/// A node serving a procedure, and the station it serves from.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub struct Provider {
180    pub node: [u8; 32],
181    pub station: [u8; 32],
182}
183
184/// A trusted advertisement: its provider, serving station, times, and the
185/// id of the KEM key it names.
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub(super) struct Candidate {
188    provider: Provider,
189    expires_at: u64,
190    created_at: u64,
191    kem_key_id: Option<[u8; KEY_ID_SIZE]>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Hash)]
195pub(super) struct ResolvedKey {
196    realm: [u8; 32],
197    procedure: String,
198    provider: [u8; 32],
199}
200
201impl Pool {
202    /// Calls a procedure at a provider that serves it, as macula 12 calls
203    /// one. A provider's ERROR comes back as
204    /// `PoolError::Link(LinkError::Provider { .. })`; when no candidate
205    /// answers, [`PoolError::NoProvider`] names each one tried.
206    pub async fn call(&self, c: Call) -> Result<Value, PoolError> {
207        let inner = &self.inner;
208        let realm_key = inner.realm_key_for(&c.realm, &c.procedure)?;
209        let timeout = if c.timeout.is_zero() {
210            DEFAULT_CALL_TIMEOUT
211        } else {
212            c.timeout
213        };
214        let deadline = Instant::now() + timeout;
215        let key = ResolvedKey {
216            realm: c.realm,
217            procedure: c.procedure.clone(),
218            provider: c.provider,
219        };
220        let candidates = bounded(deadline, inner.candidates(&key, realm_key)).await?;
221        let candidates = callable(candidates, c.confidential)?;
222        let mut tried = Vec::new();
223        let count = candidates.len();
224        for (i, cand) in candidates.into_iter().enumerate() {
225            match inner
226                .reach(&cand, candidate_share(deadline, count - i))
227                .await
228            {
229                Ok(link) => {
230                    let outcome =
231                        bounded(deadline, inner.call_at(&link, &cand, &c, deadline)).await;
232                    return inner.settled(key, cand, outcome);
233                }
234                Err(PoolError::Closed) => return Err(PoolError::Closed),
235                Err(e) => {
236                    inner.forget(&key);
237                    tried.push((cand.provider, e));
238                    if Instant::now() >= deadline {
239                        break;
240                    }
241                }
242            }
243        }
244        Err(PoolError::NoProvider(tried))
245    }
246
247    /// Every provider whose advertisement of `procedure` in `realm` the
248    /// realm's pinned key authorizes, with the station each serves from,
249    /// freshest first.
250    pub async fn providers(
251        &self,
252        realm: &[u8; 32],
253        procedure: &str,
254    ) -> Result<Vec<Provider>, PoolError> {
255        let realm_key = self.inner.realm_key_for(realm, procedure)?;
256        let key = ResolvedKey {
257            realm: *realm,
258            procedure: procedure.to_string(),
259            provider: [0; 32],
260        };
261        let found = self.inner.resolve(&key, realm_key).await?;
262        Ok(found.into_iter().map(|c| c.provider).collect())
263    }
264
265    /// Opens a streaming session at a provider of the procedure, reached as
266    /// [`Pool::call`] reaches one: the next candidate only when a station
267    /// cannot be reached, and the link's own outcome is final. The stream is
268    /// open once its STREAM_OPEN is sent; a provider's or station's refusal
269    /// arrives on its first recv.
270    pub async fn open_stream(&self, c: StreamCall) -> Result<Stream, PoolError> {
271        let inner = &self.inner;
272        let realm_key = inner.realm_key_for(&c.realm, &c.procedure)?;
273        let deadline = Instant::now() + DEFAULT_CALL_TIMEOUT;
274        let key = ResolvedKey {
275            realm: c.realm,
276            procedure: c.procedure.clone(),
277            provider: c.provider,
278        };
279        let candidates = bounded(deadline, inner.candidates(&key, realm_key)).await?;
280        let candidates = callable(candidates, c.confidential)?;
281        let mut tried = Vec::new();
282        let count = candidates.len();
283        for (i, cand) in candidates.into_iter().enumerate() {
284            match inner
285                .reach(&cand, candidate_share(deadline, count - i))
286                .await
287            {
288                Ok(link) => {
289                    let outcome = bounded(deadline, inner.open_at(&link, &cand, &c)).await;
290                    return inner.settled(key, cand, outcome);
291                }
292                Err(PoolError::Closed) => return Err(PoolError::Closed),
293                Err(e) => {
294                    inner.forget(&key);
295                    tried.push((cand.provider, e));
296                    if Instant::now() >= deadline {
297                        break;
298                    }
299                }
300            }
301        }
302        Err(PoolError::NoProvider(tried))
303    }
304
305    /// Where `station` is dialed, from the station_endpoint record the
306    /// station signed itself; one another key signed is refused.
307    pub async fn station_target(&self, station: &[u8; 32]) -> Result<Target, PoolError> {
308        self.inner.station_target(station).await
309    }
310
311    /// A link to `station`: one the pool holds, or a direct link it dials to
312    /// the address in the station's own endpoint record, pinned by its
313    /// node_id, within the default call timeout. A direct link that does not
314    /// come up on its first dial is not kept.
315    pub async fn link_to(&self, station: &[u8; 32]) -> Result<Link, PoolError> {
316        let deadline = Instant::now() + DEFAULT_CALL_TIMEOUT;
317        self.inner.link_to(station, deadline).await
318    }
319
320    /// The record under `key`, verified, from the first link that answers.
321    pub async fn find_record(&self, key: &[u8; 32]) -> Result<Verified, PoolError> {
322        self.inner
323            .first_answer(|l| async move { l.find_record(key).await })
324            .await
325    }
326
327    /// The records under `key` that verify, and how many did not, from the
328    /// first link that answers.
329    pub async fn find_records(&self, key: &[u8; 32]) -> Result<(Vec<Verified>, usize), PoolError> {
330        self.inner
331            .first_answer(|l| async move { l.find_records(key).await })
332            .await
333    }
334
335    /// The records of type `t` that verify, and how many did not, from the
336    /// first link that answers.
337    pub async fn find_records_by_type(
338        &self,
339        t: RecordType,
340    ) -> Result<(Vec<Verified>, usize), PoolError> {
341        self.inner
342            .first_answer(|l| async move { l.find_records_by_type(t).await })
343            .await
344    }
345
346    /// Puts a signed record through the first link whose station takes it.
347    pub async fn put_record(&self, wire: &[u8]) -> Result<(), PoolError> {
348        self.inner
349            .first_answer(|l| async move { l.put_record(wire).await })
350            .await
351    }
352}
353
354impl PoolInner {
355    fn remember(&self, key: ResolvedKey, cand: Candidate) {
356        self.lock().remember.insert(key, cand);
357    }
358
359    fn forget(&self, key: &ResolvedKey) {
360        self.lock().remember.remove(key);
361    }
362
363    /// The remembered candidate while it lives and its station is linked,
364    /// else the procedure's trusted advertisements from the DHT.
365    async fn candidates(
366        &self,
367        key: &ResolvedKey,
368        realm_key: Option<Vec<u8>>,
369    ) -> Result<Vec<Candidate>, PoolError> {
370        let remembered = self.lock().remember.get(key).copied();
371        if let Some(cand) = remembered {
372            if cand.expires_at as i64 > now_ms() && self.linked_to(&cand.provider.station).is_some()
373            {
374                return Ok(vec![cand]);
375            }
376        }
377        self.resolve(key, realm_key).await
378    }
379
380    /// The procedure's advertisements the realm key authorizes, by
381    /// `key.provider` when it is set, freshest first.
382    async fn resolve(
383        &self,
384        key: &ResolvedKey,
385        realm_key: Option<Vec<u8>>,
386    ) -> Result<Vec<Candidate>, PoolError> {
387        let slot = record::procedure_key(&key.realm, &key.procedure);
388        let (found, _) = self
389            .first_answer(|l| async move { l.find_records(&slot).await })
390            .await?;
391        let now = now_ms();
392        let trust = Trust {
393            profile: self.opts.identity.profile(),
394            realm_key,
395        };
396        let mut out: Vec<Candidate> = found
397            .iter()
398            .filter(|v| v.record().record_type == RecordType::PROCEDURE_ADVERTISEMENT)
399            .filter_map(|v| {
400                let ad = record::read_procedure_advertisement(v.record()).ok()?;
401                let wanted = ad.realm_id == key.realm
402                    && ad.procedure == key.procedure
403                    && (key.provider == [0; 32] || ad.advertiser_node == key.provider);
404                if !wanted || record::verify_authorization(v, &trust, now).is_err() {
405                    return None;
406                }
407                Some(Candidate {
408                    provider: Provider {
409                        node: ad.advertiser_node,
410                        station: ad.serving_station,
411                    },
412                    expires_at: v.record().expires_at,
413                    created_at: v.record().created_at,
414                    kem_key_id: ad.kem_key.map(|(_, id)| id),
415                })
416            })
417            .collect();
418        if out.is_empty() {
419            return Err(PoolError::NoProvider(Vec::new()));
420        }
421        out.sort_by_key(|c| std::cmp::Reverse(c.created_at));
422        Ok(out)
423    }
424
425    /// A link to the candidate's serving station, reached by `share`.
426    /// Nothing is sent.
427    async fn reach(self: &Arc<Self>, cand: &Candidate, share: Instant) -> Result<Link, PoolError> {
428        bounded(share, self.link_to(&cand.provider.station, share)).await
429    }
430
431    /// The outcome of a call or stream sent to `cand`, final whatever it is:
432    /// a CALL that went out is never sent again elsewhere. A candidate whose
433    /// provider answered is remembered, one that did not is forgotten.
434    fn settled<T>(
435        &self,
436        key: ResolvedKey,
437        cand: Candidate,
438        outcome: Result<T, PoolError>,
439    ) -> Result<T, PoolError> {
440        match &outcome {
441            Ok(_) | Err(PoolError::Link(LinkError::Provider { .. })) => self.remember(key, cand),
442            Err(_) => self.forget(&key),
443        }
444        outcome
445    }
446
447    /// Calls the candidate's provider on `link`, under what is left before
448    /// `deadline`.
449    async fn call_at(
450        &self,
451        link: &Link,
452        cand: &Candidate,
453        c: &Call,
454        deadline: Instant,
455    ) -> Result<Value, PoolError> {
456        let left = deadline.saturating_duration_since(Instant::now());
457        Ok(link
458            .call(station_link::Call {
459                realm: c.realm,
460                procedure: c.procedure.clone(),
461                target: cand.provider.node,
462                payload: c.payload.clone(),
463                timeout: left.max(Duration::from_millis(1)),
464                token: c.token.clone(),
465                proofs: c.proofs.clone(),
466            })
467            .await?)
468    }
469
470    /// Opens the stream at the candidate's provider on `link`.
471    async fn open_at(
472        &self,
473        link: &Link,
474        cand: &Candidate,
475        c: &StreamCall,
476    ) -> Result<Stream, PoolError> {
477        Ok(link
478            .open_stream(station_link::StreamCall {
479                realm: c.realm,
480                procedure: c.procedure.clone(),
481                target: cand.provider.node,
482                mode: c.mode,
483                payload: c.payload.clone(),
484                deadline: c.deadline,
485                token: c.token.clone(),
486                proofs: c.proofs.clone(),
487            })
488            .await?)
489    }
490
491    /// The link up now to `station`, if any.
492    fn linked_to(&self, station: &[u8; 32]) -> Option<Link> {
493        self.links()
494            .into_iter()
495            .find(|l| l.station_node_id() == *station)
496    }
497
498    pub(super) async fn link_to(
499        self: &Arc<Self>,
500        station: &[u8; 32],
501        deadline: Instant,
502    ) -> Result<Link, PoolError> {
503        if let Some(link) = self.linked_to(station) {
504            return Ok(link);
505        }
506        let (existing, direct) = {
507            let state = self.lock();
508            if state.closed {
509                return Err(PoolError::Closed);
510            }
511            let existing = state
512                .members
513                .iter()
514                .find(|m| m.target.expected_node_id == *station)
515                .cloned();
516            (existing, state.members.iter().filter(|m| m.direct).count())
517        };
518        let fresh = existing.is_none();
519        let member = match existing {
520            Some(m) => m,
521            None => {
522                if direct >= self.opts.max_direct_links {
523                    return Err(PoolError::DirectLinksFull);
524                }
525                let target = bounded(deadline, self.station_target(station)).await?;
526                self.start_member(target, true)
527            }
528        };
529        if let Some(link) = member.await_up(deadline, fresh).await {
530            return Ok(link);
531        }
532        if fresh {
533            self.drop_member(&member);
534        }
535        Err(PoolError::StationNotReached {
536            station: *station,
537            cause: member.last_error(),
538        })
539    }
540
541    async fn station_target(&self, station: &[u8; 32]) -> Result<Target, PoolError> {
542        let slot = record::station_endpoint_key(station);
543        let verified = self
544            .first_answer(|l| async move { l.find_record(&slot).await })
545            .await
546            .map_err(|e| match e {
547                PoolError::Link(link) => PoolError::NoStationEndpoint(Some(link)),
548                other => other,
549            })?;
550        let r = verified.record();
551        let signer = r.signed.as_ref().map(|s| s.key_id);
552        let endpoint = record::read_station_endpoint(r)
553            .map_err(|e| PoolError::NoStationEndpoint(Some(e.into())))?;
554        match (signer, endpoint.host_advertised.first()) {
555            (Some(signer), Some(host)) if signer == *station && endpoint.quic_port != 0 => {
556                Ok(Target {
557                    host: host.clone(),
558                    port: endpoint.quic_port,
559                    profile: self.opts.identity.profile(),
560                    expected_node_id: *station,
561                })
562            }
563            _ => Err(PoolError::NoStationEndpoint(None)),
564        }
565    }
566
567    /// Runs `ask` on the links in selection order and returns the first
568    /// answer, moving on only when a link could not carry the request: a
569    /// station's own answer, not_found included, is final.
570    pub(super) async fn first_answer<'a, T, F, Fut>(&self, ask: F) -> Result<T, PoolError>
571    where
572        F: Fn(Link) -> Fut,
573        Fut: Future<Output = Result<T, LinkError>> + 'a,
574    {
575        let links = self.links();
576        if links.is_empty() {
577            return Err(PoolError::NoLink(Vec::new()));
578        }
579        let mut errors = Vec::new();
580        for link in links {
581            match ask(link).await {
582                Err(e) if unreachable(&e) => errors.push(e),
583                answered => return answered.map_err(PoolError::Link),
584            }
585        }
586        Err(PoolError::NoLink(errors))
587    }
588}
589
590/// A failure of the link to carry a request, as opposed to the station's
591/// answer: no reply in time, or the link ended.
592fn unreachable(e: &LinkError) -> bool {
593    matches!(
594        e,
595        LinkError::CallTimeout
596            | LinkError::Closed
597            | LinkError::LivenessLost
598            | LinkError::V5DowngradeRefused
599            | LinkError::Io(_)
600            | LinkError::Goodbye(_)
601            | LinkError::StatusExpired
602            | LinkError::BindingExpired
603    )
604}
605
606/// One candidate's part of what is left before `deadline` with `left`
607/// candidates to try, at least a second, as macula shares it, and never past
608/// `deadline` itself.
609fn candidate_share(deadline: Instant, left: usize) -> Instant {
610    let now = Instant::now();
611    let remaining = deadline.saturating_duration_since(now);
612    (now + (remaining / left.max(1) as u32).max(MIN_CANDIDATE_SHARE)).min(deadline)
613}
614
615/// `work` bounded by `deadline`: past it, the call timed out.
616async fn bounded<T>(
617    deadline: Instant,
618    work: impl Future<Output = Result<T, PoolError>>,
619) -> Result<T, PoolError> {
620    tokio::time::timeout_at(deadline, work)
621        .await
622        .unwrap_or(Err(PoolError::Link(LinkError::CallTimeout)))
623}
624
625fn now_ms() -> i64 {
626    crate::uuid_v7::now_ms() as i64
627}
628
629/// The candidates a call or an open under `confidential` may reach, in their
630/// order, before anything is sent: under `Preferred` the providers none of
631/// whose advertisements names a KEM key, under `Required` none, since this
632/// SDK seals nothing yet. A provider that names a key is never called in the
633/// clear, not even through an older keyless advertisement the DHT still
634/// serves while it rotates. None left is a [`ConfidentialityError`] naming
635/// the advertised key ids.
636fn callable(
637    candidates: Vec<Candidate>,
638    confidential: Confidentiality,
639) -> Result<Vec<Candidate>, PoolError> {
640    let advertised: Vec<[u8; KEY_ID_SIZE]> =
641        candidates.iter().filter_map(|c| c.kem_key_id).collect();
642    let keyed: HashSet<[u8; 32]> = candidates
643        .iter()
644        .filter(|c| c.kem_key_id.is_some())
645        .map(|c| c.provider.node)
646        .collect();
647    let clear: Vec<Candidate> = match confidential {
648        Confidentiality::Preferred => candidates
649            .into_iter()
650            .filter(|c| !keyed.contains(&c.provider.node))
651            .collect(),
652        Confidentiality::Required => Vec::new(),
653    };
654    if clear.is_empty() {
655        return Err(PoolError::Confidentiality(ConfidentialityError {
656            reason: ConfidentialityReason::NoKemKey,
657            advertised,
658        }));
659    }
660    Ok(clear)
661}
662
663#[cfg(test)]
664mod tests {
665    use super::*;
666
667    fn cand(node: u8, kem_key_id: Option<[u8; KEY_ID_SIZE]>) -> Candidate {
668        Candidate {
669            provider: Provider {
670                node: [node; 32],
671                station: [9; 32],
672            },
673            expires_at: 0,
674            created_at: 0,
675            kem_key_id,
676        }
677    }
678
679    fn refused(r: Result<Vec<Candidate>, PoolError>) -> ConfidentialityError {
680        match r {
681            Err(PoolError::Confidentiality(e)) => e,
682            other => panic!("not a confidentiality refusal: {other:?}"),
683        }
684    }
685
686    #[test]
687    fn preferred_calls_only_the_providers_that_name_no_key() {
688        let kept = callable(
689            vec![cand(1, Some([7; 8])), cand(2, None), cand(3, None)],
690            Confidentiality::Preferred,
691        )
692        .unwrap();
693        assert_eq!(kept, vec![cand(2, None), cand(3, None)]);
694    }
695
696    #[test]
697    fn a_provider_that_names_a_key_is_never_called_in_the_clear() {
698        let e = refused(callable(
699            vec![cand(1, Some([7; 8])), cand(2, Some([8; 8]))],
700            Confidentiality::Preferred,
701        ));
702        assert_eq!(e.reason, ConfidentialityReason::NoKemKey);
703        assert_eq!(e.advertised, vec![[7; 8], [8; 8]]);
704        assert_eq!(
705            e.to_string(),
706            "confidentiality: no_kem_key 0707070707070707 0808080808080808"
707        );
708    }
709
710    #[test]
711    fn a_provider_that_names_a_key_is_not_called_through_its_older_keyless_ad() {
712        let e = refused(callable(
713            vec![cand(1, Some([7; 8])), cand(1, None)],
714            Confidentiality::Preferred,
715        ));
716        assert_eq!(e.advertised, vec![[7; 8]]);
717        let kept = callable(
718            vec![cand(1, Some([7; 8])), cand(1, None), cand(2, None)],
719            Confidentiality::Preferred,
720        )
721        .unwrap();
722        assert_eq!(kept, vec![cand(2, None)]);
723    }
724
725    #[test]
726    fn required_calls_no_provider_until_this_sdk_seals() {
727        let e = refused(callable(
728            vec![cand(1, None), cand(2, Some([7; 8]))],
729            Confidentiality::Required,
730        ));
731        assert_eq!(e.reason, ConfidentialityReason::NoKemKey);
732        assert_eq!(e.advertised, vec![[7; 8]]);
733        let e = refused(callable(vec![cand(1, None)], Confidentiality::Required));
734        assert!(e.advertised.is_empty());
735    }
736
737    #[test]
738    fn confidential_is_preferred_or_required_and_off_is_refused() {
739        assert_eq!(Confidentiality::default(), Confidentiality::Preferred);
740        assert_eq!(
741            "".parse::<Confidentiality>().unwrap(),
742            Confidentiality::Preferred
743        );
744        assert_eq!(
745            "preferred".parse::<Confidentiality>().unwrap(),
746            Confidentiality::Preferred
747        );
748        assert_eq!(
749            "required".parse::<Confidentiality>().unwrap(),
750            Confidentiality::Required
751        );
752        for refused in ["off", "Required", "none", "optional"] {
753            assert!(
754                matches!(
755                    refused.parse::<Confidentiality>(),
756                    Err(PoolError::InvalidOpts(_))
757                ),
758                "{refused}"
759            );
760        }
761    }
762}