Skip to main content

fedimint_api_client/
query.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::Debug;
3use std::future::{Future, ready};
4use std::mem;
5use std::pin::Pin;
6
7use fedimint_connectors::ServerResult;
8use fedimint_connectors::error::ServerError;
9use fedimint_core::task::{MaybeSend, MaybeSync};
10use fedimint_core::{NumPeers, PeerId, maybe_add_send, maybe_add_send_sync};
11
12/// Fedimint query strategy
13///
14/// Due to federated security model each Fedimint client API call to the
15/// Federation might require a different way to process one or more required
16/// responses from the Federation members. This trait abstracts away the details
17/// of each specific strategy for the generic client Api code.
18pub trait QueryStrategy<IR, OR = IR> {
19    /// Processes a peer's response.
20    ///
21    /// A strategy that verifies responses awaits the verification, so a heavy
22    /// one can run off the async workers.
23    fn process(
24        &mut self,
25        peer_id: PeerId,
26        response: IR,
27    ) -> impl Future<Output = QueryStep<OR>> + MaybeSend;
28
29    /// Called when a peer's request fails, so a strategy can tell "still
30    /// waiting" apart from "will never answer".
31    ///
32    /// Defaults to [`QueryStep::Continue`], leaving failed peers entirely to
33    /// the caller's error accounting, which is what every strategy other than
34    /// [`ThresholdAgreement`] wants.
35    fn process_error(&mut self, _peer_id: PeerId, _error: &ServerError) -> QueryStep<OR> {
36        QueryStep::Continue
37    }
38}
39
40/// Results from the strategy handling a response from a peer
41///
42/// Note that the implementation driving the [`QueryStrategy`] returning
43/// [`QueryStep`] is responsible from remembering and collecting errors
44/// for each peer.
45#[derive(Debug)]
46pub enum QueryStep<R> {
47    /// Retry requests to this peers
48    Retry(BTreeSet<PeerId>),
49    /// Do nothing yet, keep waiting for requests
50    Continue,
51    /// Return the successful result
52    Success(R),
53    /// A non-retryable failure has occurred
54    Failure(ServerError),
55}
56
57/// Returns when we obtain the first valid responses. RPC call errors or
58/// invalid responses are not retried.
59pub struct FilterMap<R, T> {
60    filter_map: Box<maybe_add_send_sync!(dyn Fn(R) -> ServerResult<T>)>,
61}
62
63impl<R, T> FilterMap<R, T> {
64    pub fn new(
65        filter_map: impl Fn(R) -> ServerResult<T> + MaybeSend + MaybeSync + 'static,
66    ) -> Self {
67        Self {
68            filter_map: Box::new(filter_map),
69        }
70    }
71}
72
73impl<R, T: MaybeSend> QueryStrategy<R, T> for FilterMap<R, T> {
74    fn process(
75        &mut self,
76        _peer: PeerId,
77        response: R,
78    ) -> impl Future<Output = QueryStep<T>> + MaybeSend {
79        ready(match (self.filter_map)(response) {
80            Ok(value) => QueryStep::Success(value),
81            Err(e) => QueryStep::Failure(e),
82        })
83    }
84}
85
86/// Returns when we obtain a threshold of valid responses. RPC call errors or
87/// invalid responses are not retried.
88pub struct FilterMapThreshold<R, T> {
89    filter_map: Box<maybe_add_send_sync!(dyn Fn(PeerId, R) -> FilterMapFuture<T>)>,
90    filtered_responses: BTreeMap<PeerId, T>,
91    threshold: usize,
92}
93
94type FilterMapFuture<T> = Pin<Box<maybe_add_send!(dyn Future<Output = ServerResult<T>>)>>;
95
96impl<R, T> FilterMapThreshold<R, T> {
97    /// The verifier runs once per response and is awaited, so pairings and
98    /// other heavy checks can go on the blocking pool.
99    pub fn new<Fut>(
100        verifier: impl Fn(PeerId, R) -> Fut + MaybeSend + MaybeSync + 'static,
101        num_peers: NumPeers,
102    ) -> Self
103    where
104        Fut: Future<Output = ServerResult<T>> + MaybeSend + 'static,
105    {
106        Self {
107            filter_map: Box::new(move |peer, response| Box::pin(verifier(peer, response))),
108            filtered_responses: BTreeMap::new(),
109            threshold: num_peers.threshold(),
110        }
111    }
112}
113
114impl<R: MaybeSend, T: MaybeSend> QueryStrategy<R, BTreeMap<PeerId, T>>
115    for FilterMapThreshold<R, T>
116{
117    async fn process(&mut self, peer: PeerId, response: R) -> QueryStep<BTreeMap<PeerId, T>> {
118        match (self.filter_map)(peer, response).await {
119            Ok(response) => {
120                self.filtered_responses.insert(peer, response);
121
122                if self.filtered_responses.len() == self.threshold {
123                    QueryStep::Success(mem::take(&mut self.filtered_responses))
124                } else {
125                    QueryStep::Continue
126                }
127            }
128            Err(e) => QueryStep::Failure(e),
129        }
130    }
131}
132
133/// Returns when we obtain a threshold of identical responses. Responses are not
134/// assumed to be static and may be updated by the peers; on failure to
135/// establish consensus with a threshold of responses, we retry the requests.
136/// RPC call errors are not retried.
137pub struct ThresholdConsensus<R> {
138    responses: BTreeMap<PeerId, R>,
139    retry: BTreeSet<PeerId>,
140    threshold: usize,
141}
142
143impl<R> ThresholdConsensus<R> {
144    pub fn new(num_peers: NumPeers) -> Self {
145        Self {
146            responses: BTreeMap::new(),
147            retry: BTreeSet::new(),
148            threshold: num_peers.threshold(),
149        }
150    }
151}
152
153impl<R: Eq + Clone + MaybeSend> QueryStrategy<R> for ThresholdConsensus<R> {
154    fn process(
155        &mut self,
156        peer: PeerId,
157        response: R,
158    ) -> impl Future<Output = QueryStep<R>> + MaybeSend {
159        self.responses.insert(peer, response.clone());
160
161        if self.responses.values().filter(|r| **r == response).count() == self.threshold {
162            return ready(QueryStep::Success(response));
163        }
164
165        assert!(self.retry.insert(peer));
166
167        ready(if self.retry.len() == self.threshold {
168            QueryStep::Retry(mem::take(&mut self.retry))
169        } else {
170            QueryStep::Continue
171        })
172    }
173}
174
175/// Returns the response a threshold of peers agree on, or - when they do not
176/// converge - every answer received, as `Err`.
177///
178/// Like [`ThresholdConsensus`] it counts identical responses across *all*
179/// peers rather than the first `threshold` to reply, so one lagging peer
180/// cannot mask an agreement that exists among the others.
181///
182/// Unlike it, a disagreement is never retried. Values worth querying this way
183/// are a pure function of the ordered consensus log, so a peer that has fallen
184/// behind never converges, and re-requesting it renews the transport timeout
185/// indefinitely. Asking each peer exactly once is what bounds the call.
186pub struct ThresholdAgreement<R> {
187    responses: BTreeMap<PeerId, R>,
188    errors: usize,
189    threshold: usize,
190    total: usize,
191}
192
193impl<R> ThresholdAgreement<R> {
194    pub fn new(num_peers: NumPeers) -> Self {
195        Self {
196            responses: BTreeMap::new(),
197            errors: 0,
198            threshold: num_peers.threshold(),
199            total: num_peers.total(),
200        }
201    }
202
203    /// Every peer has answered one way or the other without any value reaching
204    /// a threshold, so waiting longer cannot help.
205    fn diverged(&mut self) -> Option<QueryStep<Result<R, BTreeMap<PeerId, R>>>> {
206        if self.responses.len() + self.errors < self.total {
207            return None;
208        }
209
210        // Below a threshold of responses the useful complaint is that too few
211        // peers answered, not that they disagreed. Stay quiet and let the
212        // caller report the peer errors it collected.
213        if self.responses.len() < self.threshold {
214            return None;
215        }
216
217        Some(QueryStep::Success(Err(mem::take(&mut self.responses))))
218    }
219}
220
221impl<R: Eq + Clone + MaybeSend> QueryStrategy<R, Result<R, BTreeMap<PeerId, R>>>
222    for ThresholdAgreement<R>
223{
224    fn process(
225        &mut self,
226        peer: PeerId,
227        response: R,
228    ) -> impl Future<Output = QueryStep<Result<R, BTreeMap<PeerId, R>>>> + MaybeSend {
229        self.responses.insert(peer, response.clone());
230
231        if self.responses.values().filter(|r| **r == response).count() == self.threshold {
232            return ready(QueryStep::Success(Ok(response)));
233        }
234
235        ready(self.diverged().unwrap_or(QueryStep::Continue))
236    }
237
238    fn process_error(
239        &mut self,
240        _peer: PeerId,
241        _error: &ServerError,
242    ) -> QueryStep<Result<R, BTreeMap<PeerId, R>>> {
243        self.errors += 1;
244
245        self.diverged().unwrap_or(QueryStep::Continue)
246    }
247}
248
249#[cfg(test)]
250mod tests;