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 when a station cannot be reached or reports it cannot
10//! relay the call, and returns a provider's own answer or error as it is. A
11//! candidate that answered is remembered until its advertisement expires.
12
13use std::future::Future;
14use std::sync::Arc;
15use std::time::Duration;
16
17use tokio::time::Instant;
18
19use crate::cbor::Value;
20use crate::frame::StreamMode;
21use crate::record::{self, RecordType, Trust, Verified};
22use crate::station_link::{self, Link, LinkError, Stream, DEFAULT_CALL_TIMEOUT};
23use crate::transport::Target;
24
25use super::{Pool, PoolError, PoolInner};
26
27/// No candidate gets less than a second of a call's time.
28const MIN_CANDIDATE_SHARE: Duration = Duration::from_secs(1);
29
30/// A call to a procedure: its realm and name, the provider to call (any
31/// trusted one when zero), the payload, how long to wait
32/// ([`DEFAULT_CALL_TIMEOUT`] when zero), and a UCAN and its proofs for a
33/// gated procedure.
34#[derive(Debug, Clone, PartialEq)]
35pub struct Call {
36    pub realm: [u8; 32],
37    pub procedure: String,
38    pub provider: [u8; 32],
39    pub payload: Value,
40    pub timeout: Duration,
41    pub token: Option<Vec<u8>>,
42    pub proofs: Vec<Vec<u8>>,
43}
44
45impl Default for Call {
46    fn default() -> Self {
47        Call {
48            realm: [0; 32],
49            procedure: String::new(),
50            provider: [0; 32],
51            payload: Value::Map(Vec::new()),
52            timeout: Duration::ZERO,
53            token: None,
54            proofs: Vec::new(),
55        }
56    }
57}
58
59/// A streaming session to open: its realm and name, the provider (any
60/// trusted one when zero), the mode, the open's payload, its deadline (the
61/// link's default when zero), and a UCAN and its proofs for a gated
62/// procedure. Its default mode is server_stream.
63#[derive(Debug, Clone, PartialEq)]
64pub struct StreamCall {
65    pub realm: [u8; 32],
66    pub procedure: String,
67    pub provider: [u8; 32],
68    pub mode: StreamMode,
69    pub payload: Value,
70    pub deadline: Duration,
71    pub token: Option<Vec<u8>>,
72    pub proofs: Vec<Vec<u8>>,
73}
74
75impl Default for StreamCall {
76    fn default() -> Self {
77        StreamCall {
78            realm: [0; 32],
79            procedure: String::new(),
80            provider: [0; 32],
81            mode: StreamMode::ServerStream,
82            payload: Value::Map(Vec::new()),
83            deadline: Duration::ZERO,
84            token: None,
85            proofs: Vec::new(),
86        }
87    }
88}
89
90/// A node serving a procedure, and the station it serves from.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub struct Provider {
93    pub node: [u8; 32],
94    pub station: [u8; 32],
95}
96
97/// A trusted advertisement: its provider, serving station and times.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub(super) struct Candidate {
100    provider: Provider,
101    expires_at: u64,
102    created_at: u64,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Hash)]
106pub(super) struct ResolvedKey {
107    realm: [u8; 32],
108    procedure: String,
109    provider: [u8; 32],
110}
111
112impl Pool {
113    /// Calls a procedure at a provider that serves it, as macula 12 calls
114    /// one. A provider's ERROR comes back as
115    /// `PoolError::Link(LinkError::Provider { .. })`; when no candidate
116    /// answers, [`PoolError::NoProvider`] names each one tried.
117    pub async fn call(&self, c: Call) -> Result<Value, PoolError> {
118        let inner = &self.inner;
119        let realm_key = inner.realm_key_for(&c.realm, &c.procedure)?;
120        let timeout = if c.timeout.is_zero() {
121            DEFAULT_CALL_TIMEOUT
122        } else {
123            c.timeout
124        };
125        let deadline = Instant::now() + timeout;
126        let key = ResolvedKey {
127            realm: c.realm,
128            procedure: c.procedure.clone(),
129            provider: c.provider,
130        };
131        let candidates = bounded(deadline, inner.candidates(&key, realm_key)).await?;
132        let mut tried = Vec::new();
133        let count = candidates.len();
134        for (i, cand) in candidates.into_iter().enumerate() {
135            let share = candidate_share(deadline, count - i);
136            let outcome = bounded(share, inner.call_at(&cand, &c, share)).await;
137            match outcome {
138                Ok(v) => {
139                    inner.remember(key, cand);
140                    return Ok(v);
141                }
142                Err(e @ PoolError::Link(LinkError::Provider { .. })) => {
143                    inner.remember(key, cand);
144                    return Err(e);
145                }
146                Err(e) => {
147                    inner.forget(&key);
148                    tried.push((cand.provider, e));
149                    if Instant::now() >= deadline {
150                        break;
151                    }
152                }
153            }
154        }
155        Err(PoolError::NoProvider(tried))
156    }
157
158    /// Every provider whose advertisement of `procedure` in `realm` the
159    /// realm's pinned key authorizes, with the station each serves from,
160    /// freshest first.
161    pub async fn providers(
162        &self,
163        realm: &[u8; 32],
164        procedure: &str,
165    ) -> Result<Vec<Provider>, PoolError> {
166        let realm_key = self.inner.realm_key_for(realm, procedure)?;
167        let key = ResolvedKey {
168            realm: *realm,
169            procedure: procedure.to_string(),
170            provider: [0; 32],
171        };
172        let found = self.inner.resolve(&key, realm_key).await?;
173        Ok(found.into_iter().map(|c| c.provider).collect())
174    }
175
176    /// Opens a streaming session at a provider of the procedure, reached as
177    /// [`Pool::call`] reaches one. The stream is open once its STREAM_OPEN
178    /// is sent; a provider's or station's refusal arrives on its first recv.
179    pub async fn open_stream(&self, c: StreamCall) -> Result<Stream, PoolError> {
180        let inner = &self.inner;
181        let realm_key = inner.realm_key_for(&c.realm, &c.procedure)?;
182        let deadline = Instant::now() + DEFAULT_CALL_TIMEOUT;
183        let key = ResolvedKey {
184            realm: c.realm,
185            procedure: c.procedure.clone(),
186            provider: c.provider,
187        };
188        let candidates = bounded(deadline, inner.candidates(&key, realm_key)).await?;
189        let mut tried = Vec::new();
190        let count = candidates.len();
191        for (i, cand) in candidates.into_iter().enumerate() {
192            let share = candidate_share(deadline, count - i);
193            match bounded(share, inner.open_at(&cand, &c, share)).await {
194                Ok(stream) => {
195                    inner.remember(key, cand);
196                    return Ok(stream);
197                }
198                Err(e) => {
199                    inner.forget(&key);
200                    tried.push((cand.provider, e));
201                    if Instant::now() >= deadline {
202                        break;
203                    }
204                }
205            }
206        }
207        Err(PoolError::NoProvider(tried))
208    }
209
210    /// Where `station` is dialed, from the station_endpoint record the
211    /// station signed itself; one another key signed is refused.
212    pub async fn station_target(&self, station: &[u8; 32]) -> Result<Target, PoolError> {
213        self.inner.station_target(station).await
214    }
215
216    /// A link to `station`: one the pool holds, or a direct link it dials to
217    /// the address in the station's own endpoint record, pinned by its
218    /// node_id, within the default call timeout. A direct link that does not
219    /// come up on its first dial is not kept.
220    pub async fn link_to(&self, station: &[u8; 32]) -> Result<Link, PoolError> {
221        let deadline = Instant::now() + DEFAULT_CALL_TIMEOUT;
222        self.inner.link_to(station, deadline).await
223    }
224
225    /// The record under `key`, verified, from the first link that answers.
226    pub async fn find_record(&self, key: &[u8; 32]) -> Result<Verified, PoolError> {
227        self.inner
228            .first_answer(|l| async move { l.find_record(key).await })
229            .await
230    }
231
232    /// The records under `key` that verify, and how many did not, from the
233    /// first link that answers.
234    pub async fn find_records(&self, key: &[u8; 32]) -> Result<(Vec<Verified>, usize), PoolError> {
235        self.inner
236            .first_answer(|l| async move { l.find_records(key).await })
237            .await
238    }
239
240    /// The records of type `t` that verify, and how many did not, from the
241    /// first link that answers.
242    pub async fn find_records_by_type(
243        &self,
244        t: RecordType,
245    ) -> Result<(Vec<Verified>, usize), PoolError> {
246        self.inner
247            .first_answer(|l| async move { l.find_records_by_type(t).await })
248            .await
249    }
250
251    /// Puts a signed record through the first link whose station takes it.
252    pub async fn put_record(&self, wire: &[u8]) -> Result<(), PoolError> {
253        self.inner
254            .first_answer(|l| async move { l.put_record(wire).await })
255            .await
256    }
257}
258
259impl PoolInner {
260    fn remember(&self, key: ResolvedKey, cand: Candidate) {
261        self.lock().remember.insert(key, cand);
262    }
263
264    fn forget(&self, key: &ResolvedKey) {
265        self.lock().remember.remove(key);
266    }
267
268    /// The remembered candidate while it lives and its station is linked,
269    /// else the procedure's trusted advertisements from the DHT.
270    async fn candidates(
271        &self,
272        key: &ResolvedKey,
273        realm_key: Option<Vec<u8>>,
274    ) -> Result<Vec<Candidate>, PoolError> {
275        let remembered = self.lock().remember.get(key).copied();
276        if let Some(cand) = remembered {
277            if cand.expires_at as i64 > now_ms() && self.linked_to(&cand.provider.station).is_some()
278            {
279                return Ok(vec![cand]);
280            }
281        }
282        self.resolve(key, realm_key).await
283    }
284
285    /// The procedure's advertisements the realm key authorizes, by
286    /// `key.provider` when it is set, freshest first.
287    async fn resolve(
288        &self,
289        key: &ResolvedKey,
290        realm_key: Option<Vec<u8>>,
291    ) -> Result<Vec<Candidate>, PoolError> {
292        let slot = record::procedure_key(&key.realm, &key.procedure);
293        let (found, _) = self
294            .first_answer(|l| async move { l.find_records(&slot).await })
295            .await?;
296        let now = now_ms();
297        let trust = Trust {
298            profile: self.opts.identity.profile(),
299            realm_key,
300        };
301        let mut out: Vec<Candidate> = found
302            .iter()
303            .filter(|v| v.record().record_type == RecordType::PROCEDURE_ADVERTISEMENT)
304            .filter_map(|v| {
305                let ad = record::read_procedure_advertisement(v.record()).ok()?;
306                let wanted = ad.realm_id == key.realm
307                    && ad.procedure == key.procedure
308                    && (key.provider == [0; 32] || ad.advertiser_node == key.provider);
309                if !wanted || record::verify_authorization(v, &trust, now).is_err() {
310                    return None;
311                }
312                Some(Candidate {
313                    provider: Provider {
314                        node: ad.advertiser_node,
315                        station: ad.serving_station,
316                    },
317                    expires_at: v.record().expires_at,
318                    created_at: v.record().created_at,
319                })
320            })
321            .collect();
322        if out.is_empty() {
323            return Err(PoolError::NoProvider(Vec::new()));
324        }
325        out.sort_by_key(|c| std::cmp::Reverse(c.created_at));
326        Ok(out)
327    }
328
329    async fn call_at(
330        self: &Arc<Self>,
331        cand: &Candidate,
332        c: &Call,
333        share: Instant,
334    ) -> Result<Value, PoolError> {
335        let link = self.link_to(&cand.provider.station, share).await?;
336        let left = share.saturating_duration_since(Instant::now());
337        Ok(link
338            .call(station_link::Call {
339                realm: c.realm,
340                procedure: c.procedure.clone(),
341                target: cand.provider.node,
342                payload: c.payload.clone(),
343                timeout: left.max(Duration::from_millis(1)),
344                token: c.token.clone(),
345                proofs: c.proofs.clone(),
346            })
347            .await?)
348    }
349
350    async fn open_at(
351        self: &Arc<Self>,
352        cand: &Candidate,
353        c: &StreamCall,
354        share: Instant,
355    ) -> Result<Stream, PoolError> {
356        let link = self.link_to(&cand.provider.station, share).await?;
357        Ok(link
358            .open_stream(station_link::StreamCall {
359                realm: c.realm,
360                procedure: c.procedure.clone(),
361                target: cand.provider.node,
362                mode: c.mode,
363                payload: c.payload.clone(),
364                deadline: c.deadline,
365                token: c.token.clone(),
366                proofs: c.proofs.clone(),
367            })
368            .await?)
369    }
370
371    /// The link up now to `station`, if any.
372    fn linked_to(&self, station: &[u8; 32]) -> Option<Link> {
373        self.links()
374            .into_iter()
375            .find(|l| l.station_node_id() == *station)
376    }
377
378    pub(super) async fn link_to(
379        self: &Arc<Self>,
380        station: &[u8; 32],
381        deadline: Instant,
382    ) -> Result<Link, PoolError> {
383        if let Some(link) = self.linked_to(station) {
384            return Ok(link);
385        }
386        let (existing, direct) = {
387            let state = self.lock();
388            if state.closed {
389                return Err(PoolError::Closed);
390            }
391            let existing = state
392                .members
393                .iter()
394                .find(|m| m.target.expected_node_id == *station)
395                .cloned();
396            (existing, state.members.iter().filter(|m| m.direct).count())
397        };
398        let fresh = existing.is_none();
399        let member = match existing {
400            Some(m) => m,
401            None => {
402                if direct >= self.opts.max_direct_links {
403                    return Err(PoolError::DirectLinksFull);
404                }
405                let target = bounded(deadline, self.station_target(station)).await?;
406                self.start_member(target, true)
407            }
408        };
409        if let Some(link) = member.await_up(deadline, fresh).await {
410            return Ok(link);
411        }
412        if fresh {
413            self.drop_member(&member);
414        }
415        Err(PoolError::StationNotReached {
416            station: *station,
417            cause: member.last_error(),
418        })
419    }
420
421    async fn station_target(&self, station: &[u8; 32]) -> Result<Target, PoolError> {
422        let slot = record::station_endpoint_key(station);
423        let verified = self
424            .first_answer(|l| async move { l.find_record(&slot).await })
425            .await
426            .map_err(|e| match e {
427                PoolError::Link(link) => PoolError::NoStationEndpoint(Some(link)),
428                other => other,
429            })?;
430        let r = verified.record();
431        let signer = r.signed.as_ref().map(|s| s.key_id);
432        let endpoint = record::read_station_endpoint(r)
433            .map_err(|e| PoolError::NoStationEndpoint(Some(e.into())))?;
434        match (signer, endpoint.host_advertised.first()) {
435            (Some(signer), Some(host)) if signer == *station && endpoint.quic_port != 0 => {
436                Ok(Target {
437                    host: host.clone(),
438                    port: endpoint.quic_port,
439                    profile: self.opts.identity.profile(),
440                    expected_node_id: *station,
441                })
442            }
443            _ => Err(PoolError::NoStationEndpoint(None)),
444        }
445    }
446
447    /// Runs `ask` on the links in selection order and returns the first
448    /// answer, moving on only when a link could not carry the request: a
449    /// station's own answer, not_found included, is final.
450    pub(super) async fn first_answer<'a, T, F, Fut>(&self, ask: F) -> Result<T, PoolError>
451    where
452        F: Fn(Link) -> Fut,
453        Fut: Future<Output = Result<T, LinkError>> + 'a,
454    {
455        let links = self.links();
456        if links.is_empty() {
457            return Err(PoolError::NoLink(Vec::new()));
458        }
459        let mut errors = Vec::new();
460        for link in links {
461            match ask(link).await {
462                Err(e) if unreachable(&e) => errors.push(e),
463                answered => return answered.map_err(PoolError::Link),
464            }
465        }
466        Err(PoolError::NoLink(errors))
467    }
468}
469
470/// A failure of the link to carry a request, as opposed to the station's
471/// answer: no reply in time, or the link ended.
472fn unreachable(e: &LinkError) -> bool {
473    matches!(
474        e,
475        LinkError::CallTimeout
476            | LinkError::Closed
477            | LinkError::LivenessLost
478            | LinkError::Io(_)
479            | LinkError::Goodbye(_)
480            | LinkError::StatusExpired
481            | LinkError::BindingExpired
482    )
483}
484
485/// One candidate's part of what is left before `deadline` with `left`
486/// candidates to try, at least a second, as macula shares it, and never past
487/// `deadline` itself.
488fn candidate_share(deadline: Instant, left: usize) -> Instant {
489    let now = Instant::now();
490    let remaining = deadline.saturating_duration_since(now);
491    (now + (remaining / left.max(1) as u32).max(MIN_CANDIDATE_SHARE)).min(deadline)
492}
493
494/// `work` bounded by `deadline`: past it, the call timed out.
495async fn bounded<T>(
496    deadline: Instant,
497    work: impl Future<Output = Result<T, PoolError>>,
498) -> Result<T, PoolError> {
499    tokio::time::timeout_at(deadline, work)
500        .await
501        .unwrap_or(Err(PoolError::Link(LinkError::CallTimeout)))
502}
503
504fn now_ms() -> i64 {
505    crate::uuid_v7::now_ms() as i64
506}