1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::Debug;
3use std::mem;
4
5use fedimint_connectors::ServerResult;
6use fedimint_connectors::error::ServerError;
7use fedimint_core::task::{MaybeSend, MaybeSync};
8use fedimint_core::{NumPeers, PeerId, maybe_add_send_sync};
9
10pub trait QueryStrategy<IR, OR = IR> {
17 fn process(&mut self, peer_id: PeerId, response: IR) -> QueryStep<OR>;
18
19 fn process_error(&mut self, _peer_id: PeerId, _error: &ServerError) -> QueryStep<OR> {
26 QueryStep::Continue
27 }
28}
29
30#[derive(Debug)]
36pub enum QueryStep<R> {
37 Retry(BTreeSet<PeerId>),
39 Continue,
41 Success(R),
43 Failure(ServerError),
45}
46
47pub struct FilterMap<R, T> {
50 filter_map: Box<maybe_add_send_sync!(dyn Fn(R) -> ServerResult<T>)>,
51}
52
53impl<R, T> FilterMap<R, T> {
54 pub fn new(
55 filter_map: impl Fn(R) -> ServerResult<T> + MaybeSend + MaybeSync + 'static,
56 ) -> Self {
57 Self {
58 filter_map: Box::new(filter_map),
59 }
60 }
61}
62
63impl<R, T> QueryStrategy<R, T> for FilterMap<R, T> {
64 fn process(&mut self, _peer: PeerId, response: R) -> QueryStep<T> {
65 match (self.filter_map)(response) {
66 Ok(value) => QueryStep::Success(value),
67 Err(e) => QueryStep::Failure(e),
68 }
69 }
70}
71
72pub struct FilterMapThreshold<R, T> {
75 filter_map: Box<maybe_add_send_sync!(dyn Fn(PeerId, R) -> ServerResult<T>)>,
76 filtered_responses: BTreeMap<PeerId, T>,
77 threshold: usize,
78}
79
80impl<R, T> FilterMapThreshold<R, T> {
81 pub fn new(
82 verifier: impl Fn(PeerId, R) -> ServerResult<T> + MaybeSend + MaybeSync + 'static,
83 num_peers: NumPeers,
84 ) -> Self {
85 Self {
86 filter_map: Box::new(verifier),
87 filtered_responses: BTreeMap::new(),
88 threshold: num_peers.threshold(),
89 }
90 }
91}
92
93impl<R, T> QueryStrategy<R, BTreeMap<PeerId, T>> for FilterMapThreshold<R, T> {
94 fn process(&mut self, peer: PeerId, response: R) -> QueryStep<BTreeMap<PeerId, T>> {
95 match (self.filter_map)(peer, response) {
96 Ok(response) => {
97 self.filtered_responses.insert(peer, response);
98
99 if self.filtered_responses.len() == self.threshold {
100 QueryStep::Success(mem::take(&mut self.filtered_responses))
101 } else {
102 QueryStep::Continue
103 }
104 }
105 Err(e) => QueryStep::Failure(e),
106 }
107 }
108}
109
110pub struct ThresholdConsensus<R> {
115 responses: BTreeMap<PeerId, R>,
116 retry: BTreeSet<PeerId>,
117 threshold: usize,
118}
119
120impl<R> ThresholdConsensus<R> {
121 pub fn new(num_peers: NumPeers) -> Self {
122 Self {
123 responses: BTreeMap::new(),
124 retry: BTreeSet::new(),
125 threshold: num_peers.threshold(),
126 }
127 }
128}
129
130impl<R: Eq + Clone> QueryStrategy<R> for ThresholdConsensus<R> {
131 fn process(&mut self, peer: PeerId, response: R) -> QueryStep<R> {
132 self.responses.insert(peer, response.clone());
133
134 if self.responses.values().filter(|r| **r == response).count() == self.threshold {
135 return QueryStep::Success(response);
136 }
137
138 assert!(self.retry.insert(peer));
139
140 if self.retry.len() == self.threshold {
141 QueryStep::Retry(mem::take(&mut self.retry))
142 } else {
143 QueryStep::Continue
144 }
145 }
146}
147
148#[cfg(test)]
149fn dead_peer() -> ServerError {
150 ServerError::Connection(anyhow::anyhow!("peer is unreachable"))
151}
152
153#[test]
154fn threshold_agreement_counts_every_peer() {
155 use assert_matches::assert_matches;
156
157 let mut agreement = ThresholdAgreement::<u64>::new(NumPeers::from(4));
162
163 assert_matches!(agreement.process(PeerId::from(0), 0), QueryStep::Continue);
164 assert_matches!(agreement.process(PeerId::from(1), 1), QueryStep::Continue);
165 assert_matches!(agreement.process(PeerId::from(3), 1), QueryStep::Continue);
166 assert_matches!(
167 agreement.process(PeerId::from(2), 1),
168 QueryStep::Success(Ok(1))
169 );
170}
171
172#[test]
173fn threshold_agreement_reports_divergence_once_every_peer_has_answered() {
174 use assert_matches::assert_matches;
175
176 let mut agreement = ThresholdAgreement::<u64>::new(NumPeers::from(4));
177
178 assert_matches!(agreement.process(PeerId::from(0), 0), QueryStep::Continue);
179 assert_matches!(agreement.process(PeerId::from(1), 1), QueryStep::Continue);
180 assert_matches!(agreement.process(PeerId::from(2), 2), QueryStep::Continue);
181
182 let QueryStep::Success(Err(responses)) = agreement.process(PeerId::from(3), 3) else {
183 panic!("expected a divergence carrying every response");
184 };
185 assert_eq!(responses.len(), 4);
186}
187
188#[test]
189fn threshold_agreement_does_not_wait_on_a_peer_that_errored() {
190 use assert_matches::assert_matches;
191
192 let mut agreement = ThresholdAgreement::<u64>::new(NumPeers::from(4));
196
197 assert_matches!(agreement.process(PeerId::from(0), 0), QueryStep::Continue);
198 assert_matches!(agreement.process(PeerId::from(1), 1), QueryStep::Continue);
199 assert_matches!(agreement.process(PeerId::from(2), 1), QueryStep::Continue);
200
201 let QueryStep::Success(Err(responses)) = agreement.process_error(PeerId::from(3), &dead_peer())
202 else {
203 panic!("expected a divergence once every peer has answered");
204 };
205 assert_eq!(responses.len(), 3);
206}
207
208#[test]
209fn threshold_agreement_defers_to_peer_errors_when_too_few_answered() {
210 use assert_matches::assert_matches;
211
212 let mut agreement = ThresholdAgreement::<u64>::new(NumPeers::from(4));
216
217 assert_matches!(agreement.process(PeerId::from(0), 0), QueryStep::Continue);
218 assert_matches!(agreement.process(PeerId::from(1), 1), QueryStep::Continue);
219 assert_matches!(
220 agreement.process_error(PeerId::from(2), &dead_peer()),
221 QueryStep::Continue
222 );
223 assert_matches!(
224 agreement.process_error(PeerId::from(3), &dead_peer()),
225 QueryStep::Continue
226 );
227}
228
229pub struct ThresholdAgreement<R> {
241 responses: BTreeMap<PeerId, R>,
242 errors: usize,
243 threshold: usize,
244 total: usize,
245}
246
247impl<R> ThresholdAgreement<R> {
248 pub fn new(num_peers: NumPeers) -> Self {
249 Self {
250 responses: BTreeMap::new(),
251 errors: 0,
252 threshold: num_peers.threshold(),
253 total: num_peers.total(),
254 }
255 }
256
257 fn diverged(&mut self) -> Option<QueryStep<Result<R, BTreeMap<PeerId, R>>>> {
260 if self.responses.len() + self.errors < self.total {
261 return None;
262 }
263
264 if self.responses.len() < self.threshold {
268 return None;
269 }
270
271 Some(QueryStep::Success(Err(mem::take(&mut self.responses))))
272 }
273}
274
275impl<R: Eq + Clone> QueryStrategy<R, Result<R, BTreeMap<PeerId, R>>> for ThresholdAgreement<R> {
276 fn process(&mut self, peer: PeerId, response: R) -> QueryStep<Result<R, BTreeMap<PeerId, R>>> {
277 self.responses.insert(peer, response.clone());
278
279 if self.responses.values().filter(|r| **r == response).count() == self.threshold {
280 return QueryStep::Success(Ok(response));
281 }
282
283 self.diverged().unwrap_or(QueryStep::Continue)
284 }
285
286 fn process_error(
287 &mut self,
288 _peer: PeerId,
289 _error: &ServerError,
290 ) -> QueryStep<Result<R, BTreeMap<PeerId, R>>> {
291 self.errors += 1;
292
293 self.diverged().unwrap_or(QueryStep::Continue)
294 }
295}
296
297#[test]
298fn test_threshold_consensus() {
299 use assert_matches::assert_matches;
300
301 let mut consensus = ThresholdConsensus::<u64>::new(NumPeers::from(4));
302
303 assert_matches!(consensus.process(PeerId::from(0), 1), QueryStep::Continue);
304 assert_matches!(consensus.process(PeerId::from(1), 1), QueryStep::Continue);
305 assert_matches!(consensus.process(PeerId::from(2), 0), QueryStep::Retry(..));
306
307 assert_matches!(consensus.process(PeerId::from(0), 1), QueryStep::Continue);
308 assert_matches!(consensus.process(PeerId::from(1), 1), QueryStep::Continue);
309 assert_matches!(consensus.process(PeerId::from(2), 1), QueryStep::Success(1));
310}