1use std::hash::Hash;
2
3use hopr_types::{
4 crypto::prelude::{HalfKeyChallenge, OffchainPublicKey},
5 internal::prelude::Acknowledgement,
6};
7
8use crate::{
9 CoefficientIndex, GeneratedShare, PixGroup, PixGroupRepr, PixParams, PixSpec, PolynomialIndex, RecoveredSsa,
10 SsaCommitment, SsaCommitmentProof, SsaCommitmentState, SsaId, SsaIndex, SsaRecoveryProgress,
11 TaggedEncryptedPartialSsaShare,
12};
13
14#[derive(Clone, strum::EnumTryAs)]
28pub enum ShareResolution<P, A> {
29 RecoveredSsa(RecoveredSsa<P, A>),
31 AlmostRecoveredSsa(SsaId<P>),
33 Progress(SsaRecoveryProgress<P>),
35 InvalidShares {
41 peer: Box<OffchainPublicKey>,
43 ssa_id: SsaId<P>,
45 observed_total: u64,
47 },
48}
49
50impl<P: PartialEq, A> PartialEq for ShareResolution<P, A> {
51 fn eq(&self, other: &Self) -> bool {
52 match (self, other) {
53 (Self::RecoveredSsa(a), Self::RecoveredSsa(b)) => a == b,
54 (Self::AlmostRecoveredSsa(a), Self::AlmostRecoveredSsa(b)) => a == b,
55 (Self::Progress(a), Self::Progress(b)) => a == b,
56 (
57 Self::InvalidShares {
58 peer: p1,
59 ssa_id: id1,
60 observed_total: t1,
61 },
62 Self::InvalidShares {
63 peer: p2,
64 ssa_id: id2,
65 observed_total: t2,
66 },
67 ) => p1 == p2 && id1 == id2 && t1 == t2,
68 _ => false,
69 }
70 }
71}
72
73impl<P: Eq, A> Eq for ShareResolution<P, A> {}
74
75impl<P: std::fmt::Debug, A> std::fmt::Debug for ShareResolution<P, A> {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Self::RecoveredSsa(ssa) => f.debug_tuple("RecoveredSsa").field(ssa).finish(),
79 Self::AlmostRecoveredSsa(id) => f.debug_tuple("AlmostRecoveredSsa").field(id).finish(),
80 Self::Progress(progress) => f.debug_tuple("Progress").field(progress).finish(),
81 Self::InvalidShares {
82 peer,
83 ssa_id,
84 observed_total,
85 } => f
86 .debug_struct("InvalidShares")
87 .field("peer", peer)
88 .field("ssa_id", ssa_id)
89 .field("observed_total", observed_total)
90 .finish(),
91 }
92 }
93}
94
95impl<P: std::hash::Hash, A> Hash for ShareResolution<P, A> {
96 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
97 std::mem::discriminant(self).hash(state);
98 match self {
99 Self::RecoveredSsa(recovered) => recovered.hash(state),
100 Self::AlmostRecoveredSsa(id) => id.hash(state),
101 Self::Progress(progress) => progress.hash(state),
102 Self::InvalidShares {
103 peer,
104 ssa_id,
105 observed_total,
106 } => {
107 peer.hash(state);
108 ssa_id.hash(state);
109 observed_total.hash(state);
110 }
111 }
112 }
113}
114
115pub type ShareResolutions<S> = Vec<ShareResolution<<S as PixSpec>::Pseudonym, <S as PixSpec>::AddressPrivateKey>>;
117
118#[auto_impl::auto_impl(&, Arc, Box)]
128pub trait ExitAcknowledgementShareProcessor<S: PixSpec> {
129 type Error: std::error::Error + Send + Sync + 'static;
130
131 fn has_pending_shares(&self, _peer: &OffchainPublicKey) -> bool {
140 true
141 }
142
143 fn is_expected_error(&self, _error: &Self::Error) -> bool {
150 false
151 }
152
153 fn retire_ssa(&self, ssa_id: SsaId<S::Pseudonym>);
160
161 fn new_exit_commitment(&self, id: SsaId<S::Pseudonym>, params: PixParams) -> Result<PixGroup<S>, Self::Error>;
167
168 fn insert_coefficient_commitments(
181 &self,
182 ssa_id: SsaId<S::Pseudonym>,
183 index: CoefficientIndex,
184 proof: Option<SsaCommitmentProof<S>>,
185 commitments: impl Iterator<Item = (PolynomialIndex, PixGroupRepr<S>)>,
186 ) -> Result<SsaCommitmentState<S::Pseudonym, S::DepositAddress>, Self::Error>;
187
188 fn insert_encrypted_share(
193 &self,
194 peer: &OffchainPublicKey,
195 challenge: HalfKeyChallenge,
196 tagged_enc_share: TaggedEncryptedPartialSsaShare<S>,
197 ) -> Result<(), Self::Error>;
198
199 fn acknowledge_shares(
218 &self,
219 peer: OffchainPublicKey,
220 acks: Vec<Acknowledgement>,
221 ) -> Result<ShareResolutions<S>, Self::Error>;
222}
223
224#[auto_impl::auto_impl(&, Arc, Box)]
225pub trait EntryShareGenerator<S: PixSpec> {
226 type Error: std::error::Error + Send + Sync + 'static;
227
228 fn next_share(
235 &self,
236 pseudonym: &S::Pseudonym,
237 msg: &impl AsRef<[u8]>,
238 ) -> Result<Option<GeneratedShare<S>>, Self::Error>;
239
240 fn new_ssa_commitment(
244 &self,
245 pseudonym: &S::Pseudonym,
246 ssa_index: SsaIndex,
247 ) -> Result<SsaCommitment<S>, Self::Error>;
248}
249
250#[cfg(test)]
251mod tests {
252 use std::{
253 collections::hash_map::DefaultHasher,
254 hash::{Hash as _, Hasher},
255 };
256
257 use hopr_types::{
258 crypto::{
259 keypairs::{Keypair, OffchainKeypair},
260 prelude::{ChainKeypair, SimplePseudonym},
261 },
262 crypto_random::Randomizable,
263 };
264
265 use super::*;
266 use crate::SsaRecoveryProgress;
267
268 type Resolution = ShareResolution<SimplePseudonym, ChainKeypair>;
273
274 fn ssa_id(index: u32) -> SsaId<SimplePseudonym> {
275 SsaId::new(SimplePseudonym::random(), index.try_into().expect("non-zero index"))
276 }
277
278 fn progress(id: SsaId<SimplePseudonym>, useful: u64) -> SsaRecoveryProgress<SimplePseudonym> {
279 SsaRecoveryProgress {
280 ssa_id: id,
281 useful_shares: useful,
282 shares_seen: useful,
283 target_useful_shares: 128,
284 recovered_polynomials: 2,
285 }
286 }
287
288 fn invalid(peer: OffchainPublicKey, id: SsaId<SimplePseudonym>, total: u64) -> Resolution {
289 ShareResolution::InvalidShares {
290 peer: Box::new(peer),
291 ssa_id: id,
292 observed_total: total,
293 }
294 }
295
296 fn hash_of(value: &Resolution) -> u64 {
297 let mut hasher = DefaultHasher::new();
298 value.hash(&mut hasher);
299 hasher.finish()
300 }
301
302 #[test]
303 fn share_resolution_equality_compares_every_field_of_every_variant() {
304 let id = ssa_id(1);
305 let other_id = ssa_id(2);
306 let peer = *OffchainKeypair::random().public();
307 let other_peer = *OffchainKeypair::random().public();
308
309 let recovered = |id| {
312 Resolution::RecoveredSsa(RecoveredSsa {
313 ssa_id: id,
314 ssa: ChainKeypair::random(),
315 })
316 };
317 assert_eq!(recovered(id), recovered(id));
318 assert_ne!(recovered(id), recovered(other_id));
319
320 assert_eq!(Resolution::AlmostRecoveredSsa(id), Resolution::AlmostRecoveredSsa(id));
321 assert_ne!(
322 Resolution::AlmostRecoveredSsa(id),
323 Resolution::AlmostRecoveredSsa(other_id)
324 );
325
326 assert_eq!(
327 Resolution::Progress(progress(id, 10)),
328 Resolution::Progress(progress(id, 10))
329 );
330 assert_ne!(
331 Resolution::Progress(progress(id, 10)),
332 Resolution::Progress(progress(id, 11)),
333 "a progress snapshot differing only in useful_shares must not compare equal"
334 );
335
336 assert_eq!(invalid(peer, id, 3), invalid(peer, id, 3));
337 assert_ne!(
338 invalid(peer, id, 3),
339 invalid(other_peer, id, 3),
340 "peer must be compared"
341 );
342 assert_ne!(
343 invalid(peer, id, 3),
344 invalid(peer, other_id, 3),
345 "ssa_id must be compared"
346 );
347 assert_ne!(
348 invalid(peer, id, 3),
349 invalid(peer, id, 4),
350 "observed_total must be compared"
351 );
352 }
353
354 #[test]
355 fn share_resolution_of_different_variants_is_never_equal() {
356 let id = ssa_id(1);
357 let peer = *OffchainKeypair::random().public();
358
359 let all: [Resolution; 4] = [
360 Resolution::RecoveredSsa(RecoveredSsa {
361 ssa_id: id,
362 ssa: ChainKeypair::random(),
363 }),
364 Resolution::AlmostRecoveredSsa(id),
365 Resolution::Progress(progress(id, 10)),
366 invalid(peer, id, 3),
367 ];
368
369 for (i, left) in all.iter().enumerate() {
372 for (j, right) in all.iter().enumerate() {
373 if i != j {
374 assert_ne!(left, right, "variants {i} and {j} must not compare equal");
375 }
376 }
377 }
378 }
379
380 #[test]
381 fn share_resolution_debug_names_its_variant_and_fields() {
382 let id = ssa_id(1);
383 let peer = *OffchainKeypair::random().public();
384
385 let almost = format!("{:?}", Resolution::AlmostRecoveredSsa(id));
386 assert_eq!(almost, format!("AlmostRecoveredSsa({id:?})"));
387
388 let snapshot = progress(id, 10);
389 let progress_debug = format!("{:?}", Resolution::Progress(snapshot));
390 assert_eq!(progress_debug, format!("Progress({snapshot:?})"));
391
392 let invalid_debug = format!("{:?}", invalid(peer, id, 3));
393 assert!(invalid_debug.starts_with("InvalidShares {"), "got {invalid_debug}");
394 for field in ["peer", "ssa_id", "observed_total"] {
395 assert!(invalid_debug.contains(field), "{field} missing from {invalid_debug}");
396 }
397 }
398
399 #[test]
400 fn share_resolution_hash_agrees_with_equality() {
401 let id = ssa_id(1);
402 let other_id = ssa_id(2);
403 let peer = *OffchainKeypair::random().public();
404
405 assert_eq!(
407 hash_of(&Resolution::AlmostRecoveredSsa(id)),
408 hash_of(&Resolution::AlmostRecoveredSsa(id))
409 );
410 assert_eq!(
411 hash_of(&Resolution::Progress(progress(id, 10))),
412 hash_of(&Resolution::Progress(progress(id, 10)))
413 );
414 assert_eq!(hash_of(&invalid(peer, id, 3)), hash_of(&invalid(peer, id, 3)));
415 assert_eq!(
416 hash_of(&Resolution::RecoveredSsa(RecoveredSsa {
417 ssa_id: id,
418 ssa: ChainKeypair::random(),
419 })),
420 hash_of(&Resolution::RecoveredSsa(RecoveredSsa {
421 ssa_id: id,
422 ssa: ChainKeypair::random(),
423 })),
424 "RecoveredSsa hashes the id only, so the key must not perturb it"
425 );
426
427 assert_ne!(
429 hash_of(&Resolution::AlmostRecoveredSsa(id)),
430 hash_of(&Resolution::RecoveredSsa(RecoveredSsa {
431 ssa_id: id,
432 ssa: ChainKeypair::random(),
433 })),
434 "the discriminant must be hashed, or same-id variants collide"
435 );
436
437 assert_ne!(
439 hash_of(&Resolution::AlmostRecoveredSsa(id)),
440 hash_of(&Resolution::AlmostRecoveredSsa(other_id))
441 );
442 assert_ne!(
443 hash_of(&Resolution::Progress(progress(id, 10))),
444 hash_of(&Resolution::Progress(progress(id, 11)))
445 );
446 assert_ne!(hash_of(&invalid(peer, id, 3)), hash_of(&invalid(peer, id, 4)));
447 }
448
449 #[test]
453 fn exit_processor_defaults_are_conservative() {
454 struct Minimal;
455
456 #[derive(Debug, thiserror::Error)]
457 #[error("nope")]
458 struct MinimalError;
459
460 impl ExitAcknowledgementShareProcessor<crate::tests::TestSpec> for Minimal {
461 type Error = MinimalError;
462
463 fn retire_ssa(&self, _ssa_id: SsaId<SimplePseudonym>) {}
464
465 fn new_exit_commitment(
466 &self,
467 _id: SsaId<SimplePseudonym>,
468 _params: PixParams,
469 ) -> Result<PixGroup<crate::tests::TestSpec>, Self::Error> {
470 Err(MinimalError)
471 }
472
473 fn insert_coefficient_commitments(
474 &self,
475 _ssa_id: SsaId<SimplePseudonym>,
476 _index: CoefficientIndex,
477 _proof: Option<SsaCommitmentProof<crate::tests::TestSpec>>,
478 _commitments: impl Iterator<Item = (PolynomialIndex, PixGroupRepr<crate::tests::TestSpec>)>,
479 ) -> Result<SsaCommitmentState<SimplePseudonym, hopr_types::primitive::prelude::Address>, Self::Error>
480 {
481 Err(MinimalError)
482 }
483
484 fn insert_encrypted_share(
485 &self,
486 _peer: &OffchainPublicKey,
487 _challenge: HalfKeyChallenge,
488 _tagged_enc_share: TaggedEncryptedPartialSsaShare<crate::tests::TestSpec>,
489 ) -> Result<(), Self::Error> {
490 Err(MinimalError)
491 }
492
493 fn acknowledge_shares(
494 &self,
495 _peer: OffchainPublicKey,
496 _acks: Vec<Acknowledgement>,
497 ) -> Result<ShareResolutions<crate::tests::TestSpec>, Self::Error> {
498 Err(MinimalError)
499 }
500 }
501
502 let peer = *OffchainKeypair::random().public();
503 assert!(
504 Minimal.has_pending_shares(&peer),
505 "the default must assume shares may be pending, so the caller still calls in"
506 );
507 assert!(
508 !Minimal.is_expected_error(&MinimalError),
509 "the default must treat every error as unexpected, so nothing is silently downgraded"
510 );
511 }
512
513 #[test]
514 fn debug_redaction_share_resolution_recovered_ssa() {
515 let pseudonym = SimplePseudonym::random();
518 let ssa_id = SsaId::new(pseudonym, 1.try_into().unwrap());
519 let dummy_key = ChainKeypair::random();
520 let recovered = RecoveredSsa { ssa_id, ssa: dummy_key };
521 let recovered_debug = format!("{:?}", recovered);
522 let resolution = ShareResolution::RecoveredSsa(recovered);
523 let debug = format!("{:?}", resolution);
524
525 assert!(debug.contains("RecoveredSsa"));
526 assert_eq!(
528 debug,
529 format!("RecoveredSsa({recovered_debug})"),
530 "ShareResolution::RecoveredSsa Debug must perfectly delegate to RecoveredSsa Debug"
531 );
532 }
533}