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