1use std::sync::Arc;
24
25use auths_keri::{Prefix, Said};
26use tokio::time::{Duration, timeout};
27
28use super::async_provider::AsyncWitnessProvider;
29use super::error::{DuplicityEvidence, WitnessError};
30use super::receipt::StoredReceipt;
31use super::verify::{VerifiedReceipt, verify_receipt};
32
33#[derive(Debug, thiserror::Error)]
35#[non_exhaustive]
36pub enum CollectionError {
37 #[error("duplicity detected: {0}")]
39 Duplicity(DuplicityEvidence),
40
41 #[error("threshold not met: got {got} receipts, need {required}")]
43 ThresholdNotMet {
44 got: usize,
46 required: usize,
48 errors: Vec<(String, WitnessError)>,
50 },
51
52 #[error("all witnesses failed")]
54 AllFailed {
55 errors: Vec<(String, WitnessError)>,
57 },
58
59 #[error("no witnesses configured")]
61 NoWitnesses,
62}
63
64pub struct ReceiptCollector {
89 witnesses: Vec<(Prefix, Arc<dyn AsyncWitnessProvider>)>,
92 threshold: usize,
94 timeout_ms: u64,
96}
97
98impl ReceiptCollector {
99 pub fn new(
109 witnesses: Vec<(Prefix, Arc<dyn AsyncWitnessProvider>)>,
110 threshold: usize,
111 timeout_ms: u64,
112 ) -> Self {
113 Self {
114 witnesses,
115 threshold,
116 timeout_ms,
117 }
118 }
119
120 pub fn witness_count(&self) -> usize {
122 self.witnesses.len()
123 }
124
125 pub fn threshold(&self) -> usize {
127 self.threshold
128 }
129
130 pub async fn collect(
149 &self,
150 prefix: &Prefix,
151 event_said: &Said,
152 event_json: &[u8],
153 ) -> Result<Vec<VerifiedReceipt>, CollectionError> {
154 if self.witnesses.is_empty() {
155 return Err(CollectionError::NoWitnesses);
156 }
157
158 let mut handles = Vec::with_capacity(self.witnesses.len());
160 let timeout_duration = Duration::from_millis(self.timeout_ms);
161
162 for (idx, (aid, witness)) in self.witnesses.iter().enumerate() {
163 let witness = Arc::clone(witness);
164 let aid = aid.clone();
165 let prefix = prefix.clone();
166 let event_json = event_json.to_vec();
167
168 let handle = tokio::spawn(async move {
169 let result =
170 timeout(timeout_duration, witness.submit_event(&prefix, &event_json)).await;
171
172 match result {
173 Ok(Ok(signed)) => Ok((
174 idx,
175 StoredReceipt {
176 signed,
177 witness: aid,
178 },
179 )),
180 Ok(Err(e)) => Err((idx, e)),
181 Err(_) => Err((
182 idx,
183 WitnessError::Timeout(timeout_duration.as_millis() as u64),
184 )),
185 }
186 });
187
188 handles.push(handle);
189 }
190
191 let mut receipts: Vec<VerifiedReceipt> = Vec::new();
193 let mut errors: Vec<(String, WitnessError)> = Vec::new();
194
195 for handle in handles {
196 match handle.await {
197 Ok(Ok((idx, stored))) => match verify_receipt(stored, event_said) {
198 Ok(verified) => receipts.push(verified),
199 Err(e) => errors.push((format!("witness_{idx}"), e)),
202 },
203 Ok(Err((idx, e))) => {
204 if let WitnessError::Duplicity(evidence) = e {
206 return Err(CollectionError::Duplicity(evidence));
207 }
208 errors.push((format!("witness_{}", idx), e));
209 }
210 Err(join_err) => {
211 errors.push((
212 "unknown".to_string(),
213 WitnessError::Network(format!("task join error: {}", join_err)),
214 ));
215 }
216 }
217 }
218
219 if receipts.len() >= self.threshold {
221 Ok(receipts)
222 } else if receipts.is_empty() && !errors.is_empty() {
223 Err(CollectionError::AllFailed { errors })
224 } else {
225 Err(CollectionError::ThresholdNotMet {
226 got: receipts.len(),
227 required: self.threshold,
228 errors,
229 })
230 }
231 }
232}
233
234#[derive(Default)]
236pub struct ReceiptCollectorBuilder {
237 witnesses: Vec<(Prefix, Arc<dyn AsyncWitnessProvider>)>,
238 threshold: Option<usize>,
239 timeout_ms: Option<u64>,
240}
241
242impl std::fmt::Debug for ReceiptCollectorBuilder {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 f.debug_struct("ReceiptCollectorBuilder")
245 .field("witnesses_count", &self.witnesses.len())
246 .field("threshold", &self.threshold)
247 .field("timeout_ms", &self.timeout_ms)
248 .finish()
249 }
250}
251
252impl ReceiptCollectorBuilder {
253 pub fn new() -> Self {
255 Self::default()
256 }
257
258 pub fn witness(mut self, aid: Prefix, witness: Arc<dyn AsyncWitnessProvider>) -> Self {
260 self.witnesses.push((aid, witness));
261 self
262 }
263
264 pub fn witnesses(
266 mut self,
267 witnesses: impl IntoIterator<Item = (Prefix, Arc<dyn AsyncWitnessProvider>)>,
268 ) -> Self {
269 self.witnesses.extend(witnesses);
270 self
271 }
272
273 pub fn threshold(mut self, threshold: usize) -> Self {
275 self.threshold = Some(threshold);
276 self
277 }
278
279 pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
281 self.timeout_ms = Some(timeout_ms);
282 self
283 }
284
285 pub fn build(self) -> ReceiptCollector {
290 ReceiptCollector {
291 witnesses: self.witnesses,
292 threshold: self.threshold.unwrap_or(1),
293 timeout_ms: self.timeout_ms.unwrap_or(5000),
294 }
295 }
296}
297
298#[cfg(test)]
299#[allow(clippy::unwrap_used)]
300mod tests {
301 use super::*;
302 use crate::witness::NoOpAsyncWitness;
303 use async_trait::async_trait;
304 use auths_keri::KeriPublicKey;
305 use auths_keri::witness::{EventHash, Receipt, ReceiptTag, SignedReceipt};
306 use auths_keri::{KeriSequence, VersionString};
307 use ring::signature::{Ed25519KeyPair, KeyPair};
308
309 const EVENT_SAID: &str = "EEvent00000000000000000000000000000000000000";
310 const OTHER_SAID: &str = "EOther00000000000000000000000000000000000000";
311
312 fn event_said() -> Said {
313 Said::new_unchecked(EVENT_SAID.to_string())
314 }
315
316 fn keypair_and_aid() -> (Arc<Ed25519KeyPair>, Prefix) {
318 let rng = ring::rand::SystemRandom::new();
319 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
320 let kp = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
321 let aid = KeriPublicKey::ed25519(kp.public_key().as_ref())
322 .unwrap()
323 .to_qb64()
324 .unwrap();
325 (Arc::new(kp), Prefix::new_unchecked(aid))
326 }
327
328 struct SigningWitness {
330 kp: Arc<Ed25519KeyPair>,
331 said: Said,
332 }
333
334 #[async_trait]
335 impl AsyncWitnessProvider for SigningWitness {
336 async fn submit_event(
337 &self,
338 prefix: &Prefix,
339 _event_json: &[u8],
340 ) -> Result<SignedReceipt, WitnessError> {
341 let receipt = Receipt {
342 v: VersionString::placeholder(),
343 t: ReceiptTag,
344 d: self.said.clone(),
345 i: prefix.clone(),
346 s: KeriSequence::new(0),
347 };
348 let payload = serde_json::to_vec(&receipt)
349 .map_err(|e| WitnessError::Serialization(e.to_string()))?;
350 let signature = self.kp.sign(&payload).as_ref().to_vec();
351 Ok(SignedReceipt { receipt, signature })
352 }
353
354 async fn observe_identity_head(
355 &self,
356 _prefix: &Prefix,
357 ) -> Result<Option<EventHash>, WitnessError> {
358 Ok(None)
359 }
360
361 async fn get_receipt(
362 &self,
363 _prefix: &Prefix,
364 _event_said: &Said,
365 ) -> Result<Option<Receipt>, WitnessError> {
366 Ok(None)
367 }
368
369 fn quorum(&self) -> usize {
370 0
371 }
372 }
373
374 fn witness_pair(
375 kp: Arc<Ed25519KeyPair>,
376 aid: Prefix,
377 said: Said,
378 ) -> (Prefix, Arc<dyn AsyncWitnessProvider>) {
379 (
380 aid,
381 Arc::new(SigningWitness { kp, said }) as Arc<dyn AsyncWitnessProvider>,
382 )
383 }
384
385 fn honest() -> (Prefix, Arc<dyn AsyncWitnessProvider>) {
387 let (kp, aid) = keypair_and_aid();
388 witness_pair(kp, aid, event_said())
389 }
390
391 fn forged() -> (Prefix, Arc<dyn AsyncWitnessProvider>) {
393 let (signing_kp, _signing_aid) = keypair_and_aid();
394 let (_pinned_kp, pinned_aid) = keypair_and_aid();
395 witness_pair(signing_kp, pinned_aid, event_said())
396 }
397
398 #[tokio::test]
399 async fn verified_receipts_reach_quorum() {
400 let collector = ReceiptCollector::new(vec![honest(), honest(), honest()], 2, 5000);
401 let prefix = Prefix::new_unchecked("EPrefix".into());
402 let receipts = collector
403 .collect(&prefix, &event_said(), b"{}")
404 .await
405 .unwrap();
406
407 assert!(receipts.len() >= 2);
408 assert!(receipts.iter().all(|r| !r.witness.as_str().is_empty()));
409 }
410
411 #[tokio::test]
412 async fn forged_receipt_does_not_count() {
413 let collector = ReceiptCollector::new(vec![honest(), honest(), forged()], 2, 5000);
415 let prefix = Prefix::new_unchecked("EPrefix".into());
416 let receipts = collector
417 .collect(&prefix, &event_said(), b"{}")
418 .await
419 .unwrap();
420
421 assert_eq!(receipts.len(), 2, "forged receipt must be dropped");
422 }
423
424 #[tokio::test]
425 async fn forged_receipt_drops_below_threshold() {
426 let collector = ReceiptCollector::new(vec![honest(), forged()], 2, 5000);
428 let prefix = Prefix::new_unchecked("EPrefix".into());
429 let result = collector.collect(&prefix, &event_said(), b"{}").await;
430
431 assert!(matches!(
432 result,
433 Err(CollectionError::ThresholdNotMet {
434 got: 1,
435 required: 2,
436 ..
437 })
438 ));
439 }
440
441 #[tokio::test]
442 async fn wrong_said_receipt_dropped() {
443 let (kp, aid) = keypair_and_aid();
444 let wrong = witness_pair(kp, aid, Said::new_unchecked(OTHER_SAID.to_string()));
445 let collector = ReceiptCollector::new(vec![honest(), wrong], 2, 5000);
446 let prefix = Prefix::new_unchecked("EPrefix".into());
447 let result = collector.collect(&prefix, &event_said(), b"{}").await;
448
449 assert!(matches!(
450 result,
451 Err(CollectionError::ThresholdNotMet { got: 1, .. })
452 ));
453 }
454
455 #[tokio::test]
456 async fn no_witnesses_error() {
457 let collector = ReceiptCollector::new(vec![], 1, 5000);
458 let prefix = Prefix::new_unchecked("EPrefix".into());
459 let result = collector.collect(&prefix, &event_said(), b"{}").await;
460
461 assert!(matches!(result, Err(CollectionError::NoWitnesses)));
462 }
463
464 #[tokio::test]
465 async fn builder_pattern() {
466 let (_, aid0) = keypair_and_aid();
467 let (_, aid1) = keypair_and_aid();
468 let collector = ReceiptCollectorBuilder::new()
469 .witness(aid0, Arc::new(NoOpAsyncWitness))
470 .witness(aid1, Arc::new(NoOpAsyncWitness))
471 .threshold(2)
472 .timeout_ms(1000)
473 .build();
474
475 assert_eq!(collector.witness_count(), 2);
476 assert_eq!(collector.threshold(), 2);
477 }
478
479 #[test]
480 fn collection_error_display() {
481 let err = CollectionError::ThresholdNotMet {
482 got: 1,
483 required: 2,
484 errors: vec![],
485 };
486 assert!(format!("{}", err).contains("threshold not met"));
487
488 let err = CollectionError::NoWitnesses;
489 assert!(format!("{}", err).contains("no witnesses"));
490 }
491}