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