1use std::{
2 collections::{BTreeMap, HashMap},
3 num::NonZero,
4 ops::Add,
5};
6
7use hopr_types::{
8 crypto::{
9 crypto_traits::{
10 self, KeyIvInit, StreamCipher,
11 elliptic_curve::{Curve, PrimeField},
12 },
13 prelude::HalfKey,
14 primitives::Blake3,
15 },
16 primitive::{
17 hybrid_array::{
18 Array, ArraySize,
19 typenum::{Sum, U, Unsigned},
20 },
21 prelude::{BytesRepresentable, GeneralError},
22 },
23};
24
25use crate::{
26 ExitAcknowledgementShareProcessor, Group, GroupEncoding, PixGroup, PixGroupRepr, PixScalar, PixSpec,
27 SsaPartCommitment, errors, errors::PixError,
28};
29
30pub type RawSsaIndex = u32;
32
33pub type SsaIndex = NonZero<RawSsaIndex>;
37
38pub type PolynomialIndex = u16;
42
43pub type CoefficientIndex = u16;
47
48const SSA_INDEX_SIZE: usize = size_of::<SsaIndex>();
50
51const POLY_INDEX_SIZE: usize = size_of::<PolynomialIndex>();
53
54pub type SsaPolyIndexPrefixSize = Sum<U<SSA_INDEX_SIZE>, U<POLY_INDEX_SIZE>>;
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
67pub struct SsaId<P> {
68 pseudonym: P,
69 ssa_index: SsaIndex,
70}
71
72impl<P> SsaId<P> {
73 pub fn new(pseudonym: P, ssa_index: SsaIndex) -> Self {
75 Self { pseudonym, ssa_index }
76 }
77
78 #[inline]
80 pub fn pseudonym(&self) -> &P {
81 &self.pseudonym
82 }
83
84 #[inline]
86 pub fn ssa_index(&self) -> SsaIndex {
87 self.ssa_index
88 }
89}
90
91impl<P: std::fmt::Display> std::fmt::Display for SsaId<P> {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 write!(f, "{}-ssa#{}", self.pseudonym, self.ssa_index)
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106pub struct SsaPolynomialId<P> {
107 id: SsaId<P>,
108 poly_index: PolynomialIndex,
109}
110
111pub type TransposedVerifiers<S> = HashMap<CoefficientIndex, Vec<(PolynomialIndex, PixGroupRepr<S>)>>;
119
120impl<P> SsaPolynomialId<P> {
121 pub fn new(id: SsaId<P>, poly_index: PolynomialIndex) -> Self {
123 Self { id, poly_index }
124 }
125
126 #[inline]
128 pub fn pseudonym(&self) -> &P {
129 &self.id.pseudonym
130 }
131
132 #[inline]
134 pub fn ssa_index(&self) -> SsaIndex {
135 self.id.ssa_index
136 }
137
138 #[inline]
140 pub fn poly_index(&self) -> PolynomialIndex {
141 self.poly_index
142 }
143}
144
145impl<P> AsRef<SsaId<P>> for SsaPolynomialId<P> {
146 fn as_ref(&self) -> &SsaId<P> {
147 &self.id
148 }
149}
150
151impl<P: std::fmt::Display> std::fmt::Display for SsaPolynomialId<P> {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 write!(f, "{}:{}", self.id, self.poly_index)
154 }
155}
156
157#[derive(Clone, Default, Hash, PartialEq, Eq)]
167pub struct PartialSsaShare<S: PixSpec>(pub(crate) <PixScalar<S> as PrimeField>::Repr);
168
169impl<S: PixSpec> std::fmt::Debug for PartialSsaShare<S> {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.debug_struct("PartialSsaShare").finish_non_exhaustive()
172 }
173}
174
175impl<S: PixSpec> PartialSsaShare<S> {
176 pub fn encrypt(
178 mut self,
179 spi: &SsaPolynomialId<S::Pseudonym>,
180 ack_key: &HalfKey,
181 ) -> errors::Result<EncryptedPartialSsaShare<S>, S::Pseudonym>
182 where
183 FieldBytesSize<S>: Add<SsaPolyIndexPrefixSize>,
184 EncShareSize<S>: ArraySize,
185 {
186 let mut cipher = derive_ssa_encryption_key::<S>(spi, ack_key)?;
187 cipher.apply_keystream(self.0.as_mut());
188
189 let mut out = Array::<u8, EncShareSize<S>>::default();
190 out[0..size_of::<SsaIndex>()].copy_from_slice(&spi.ssa_index().get().to_be_bytes());
191 out[size_of::<SsaIndex>()..size_of::<SsaIndex>() + size_of::<PolynomialIndex>()]
192 .copy_from_slice(&spi.poly_index().to_be_bytes());
193 out[size_of::<SsaIndex>() + size_of::<PolynomialIndex>()..].copy_from_slice(self.0.as_ref());
194 Ok(EncryptedPartialSsaShare(out))
195 }
196}
197
198impl<S: PixSpec> AsRef<<PixScalar<S> as PrimeField>::Repr> for PartialSsaShare<S> {
199 fn as_ref(&self) -> &<PixScalar<S> as PrimeField>::Repr {
200 &self.0
201 }
202}
203
204fn derive_ssa_encryption_key<S: PixSpec>(
205 spi: &SsaPolynomialId<S::Pseudonym>,
206 ack: &HalfKey,
207) -> errors::Result<S::Cipher, S::Pseudonym> {
208 let mut output = Blake3::new_derive_key(S::KEY_DERIVATION_CONTEXT)
209 .update_reader(ack.as_ref())
210 .and_then(|h| h.update_reader(spi.id.pseudonym.as_ref()))
211 .and_then(|h| h.update_reader(spi.id.ssa_index.get().to_be_bytes().as_ref()))
212 .and_then(|h| h.update_reader(spi.poly_index.to_be_bytes().as_ref()))
213 .map_err(|_| hopr_types::crypto::errors::CryptoError::InvalidInputValue("invalid ssa encryption key"))?
214 .finalize_xof();
215
216 let mut key = crypto_traits::Key::<S::Cipher>::default();
217 let mut iv = crypto_traits::Iv::<S::Cipher>::default();
218
219 let mut out = vec![0u8; key.len() + iv.len()];
220 output.fill(&mut out);
221
222 let (v_iv, v_key) = out.split_at(iv.len());
223 iv.copy_from_slice(v_iv);
224 key.copy_from_slice(v_key);
225
226 Ok(S::Cipher::new(&key, &iv))
227}
228
229pub type FieldBytesSize<S> = <<S as PixSpec>::Curve as Curve>::FieldBytesSize;
231
232pub type EncShareSize<S> = Sum<FieldBytesSize<S>, SsaPolyIndexPrefixSize>;
235
236#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
246pub struct EncryptedPartialSsaShare<S: PixSpec>(Array<u8, EncShareSize<S>>)
247where
248 FieldBytesSize<S>: Add<SsaPolyIndexPrefixSize>,
249 EncShareSize<S>: ArraySize;
250
251#[cfg(feature = "serde")]
252impl<S: PixSpec> serde::Serialize for EncryptedPartialSsaShare<S>
253where
254 FieldBytesSize<S>: Add<SsaPolyIndexPrefixSize>,
255 EncShareSize<S>: ArraySize,
256{
257 fn serialize<Ser: serde::Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
258 serde_bytes::Bytes::new(self.0.as_ref()).serialize(serializer)
259 }
260}
261
262#[cfg(feature = "serde")]
263impl<'de, S: PixSpec> serde::Deserialize<'de> for EncryptedPartialSsaShare<S>
264where
265 FieldBytesSize<S>: Add<SsaPolyIndexPrefixSize>,
266 EncShareSize<S>: ArraySize,
267{
268 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
269 let bytes = <&serde_bytes::Bytes as serde::Deserialize>::deserialize(deserializer)?;
270 Self::try_from(bytes.as_ref()).map_err(serde::de::Error::custom)
271 }
272}
273
274impl<S: PixSpec> EncryptedPartialSsaShare<S>
275where
276 FieldBytesSize<S>: Add<SsaPolyIndexPrefixSize>,
277 EncShareSize<S>: ArraySize,
278{
279 pub fn indices(&self) -> Option<(SsaIndex, PolynomialIndex)> {
283 let ssa_index: Option<SsaIndex> =
284 RawSsaIndex::from_be_bytes(self.0[0..size_of::<RawSsaIndex>()].try_into().ok()?)
285 .try_into()
286 .ok();
287
288 let poly_index = PolynomialIndex::from_be_bytes(
289 self.0[size_of::<RawSsaIndex>()..size_of::<RawSsaIndex>() + size_of::<PolynomialIndex>()]
290 .try_into()
291 .ok()?,
292 );
293
294 ssa_index.map(|ssa_index| (ssa_index, poly_index))
295 }
296
297 pub(crate) fn decrypt(
302 self,
303 pseudonym: &S::Pseudonym,
304 ack_key: &HalfKey,
305 ) -> errors::Result<PartialSsaShare<S>, S::Pseudonym> {
306 if let Some((ssa_index, poly_index)) = self.indices() {
307 let spi = SsaPolynomialId::new(SsaId::new(*pseudonym, ssa_index), poly_index);
308 let mut cipher = derive_ssa_encryption_key::<S>(&spi, ack_key)?;
309 let mut share = <PixScalar<S> as PrimeField>::Repr::default();
310 share.copy_from_slice(&self.0[SsaPolyIndexPrefixSize::USIZE..]);
311 cipher.apply_keystream(share.as_mut());
312 Ok(PartialSsaShare(share))
313 } else {
314 Err(PixError::ShareIsEmpty)
315 }
316 }
317
318 #[inline]
320 pub fn is_empty(&self) -> bool {
321 self.0 == Array::<u8, EncShareSize<S>>::default() || self.indices().is_none()
322 }
323}
324
325impl<S: PixSpec + Copy> Copy for EncryptedPartialSsaShare<S>
326where
327 FieldBytesSize<S>: Add<SsaPolyIndexPrefixSize>,
328 EncShareSize<S>: ArraySize,
329 Array<u8, EncShareSize<S>>: Copy,
330{
331}
332
333impl<S: PixSpec> AsRef<[u8]> for EncryptedPartialSsaShare<S>
334where
335 FieldBytesSize<S>: Add<SsaPolyIndexPrefixSize>,
336 EncShareSize<S>: ArraySize,
337{
338 fn as_ref(&self) -> &[u8] {
339 &self.0
340 }
341}
342
343impl<'a, S: PixSpec> TryFrom<&'a [u8]> for EncryptedPartialSsaShare<S>
344where
345 FieldBytesSize<S>: Add<SsaPolyIndexPrefixSize>,
346 EncShareSize<S>: ArraySize,
347{
348 type Error = GeneralError;
349
350 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
351 Array::try_from(value)
352 .map(Self)
353 .map_err(|_| GeneralError::ParseError("EncryptedPartialSsaShare.size".into()))
354 }
355}
356
357impl<S: PixSpec> BytesRepresentable for EncryptedPartialSsaShare<S>
358where
359 FieldBytesSize<S>: Add<SsaPolyIndexPrefixSize>,
360 EncShareSize<S>: ArraySize,
361{
362 const SIZE: usize = EncShareSize::<S>::USIZE;
363}
364
365#[derive(Clone, Debug, PartialEq, Eq)]
368#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
369#[cfg_attr(
370 feature = "serde",
371 serde(bound(
372 serialize = "P: serde::Serialize, T: serde::Serialize, EncryptedPartialSsaShare<S>: serde::Serialize",
373 deserialize = "P: serde::Deserialize<'de>, T: serde::Deserialize<'de>, EncryptedPartialSsaShare<S>: \
374 serde::Deserialize<'de>"
375 ))
376)]
377pub struct TaggedEncryptedPartialSsaShare<S: PixSpec, P = <S as PixSpec>::Pseudonym, T = PixScalar<S>> {
378 pub pseudonym: P,
380 pub nonce: T,
384 pub partial_share: EncryptedPartialSsaShare<S>,
386}
387
388impl<S: PixSpec> TaggedEncryptedPartialSsaShare<S, S::Pseudonym, PixScalar<S>> {
389 pub fn new(
393 pseudonym: S::Pseudonym,
394 nonce: &impl AsRef<[u8]>,
395 partial_share: EncryptedPartialSsaShare<S>,
396 ) -> errors::Result<Self, S::Pseudonym> {
397 if let Some((ssa_index, poly_index)) = partial_share.indices() {
398 Ok(Self {
399 pseudonym,
400 nonce: S::msg_to_scalar(
401 &SsaPolynomialId::new(SsaId::new(pseudonym, ssa_index), poly_index),
402 nonce.as_ref(),
403 )?,
404 partial_share,
405 })
406 } else {
407 Err(PixError::ShareIsEmpty)
408 }
409 }
410}
411
412impl<S: PixSpec> TaggedEncryptedPartialSsaShare<S, S::Pseudonym, PixScalar<S>> {
413 #[inline]
417 pub fn ssa_id(&self) -> Option<SsaId<S::Pseudonym>> {
418 self.partial_share
419 .indices()
420 .map(|(ssa_index, _)| SsaId::new(self.pseudonym, ssa_index))
421 }
422
423 #[inline]
427 pub fn ssa_polynomial_id(&self) -> Option<SsaPolynomialId<S::Pseudonym>> {
428 self.partial_share
429 .indices()
430 .map(|(ssa_index, poly_index)| SsaPolynomialId::new(SsaId::new(self.pseudonym, ssa_index), poly_index))
431 }
432}
433
434impl<S: PixSpec + Copy, P: Copy, T: Copy> Copy for TaggedEncryptedPartialSsaShare<S, P, T> where
435 EncryptedPartialSsaShare<S>: Copy
436{
437}
438
439#[derive(Clone, PartialEq, Eq)]
441pub struct GeneratedShare<S: PixSpec, P = <S as PixSpec>::Pseudonym> {
442 pub id: SsaPolynomialId<P>,
444 pub share: PartialSsaShare<S>,
446}
447
448impl<S: PixSpec, P: std::fmt::Debug> std::fmt::Debug for GeneratedShare<S, P> {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 f.debug_struct("GeneratedShare")
451 .field("id", &self.id)
452 .finish_non_exhaustive()
453 }
454}
455
456impl<S: PixSpec> GeneratedShare<S, S::Pseudonym> {
457 #[inline]
459 pub fn encrypt(self, ack: &HalfKey) -> errors::Result<EncryptedPartialSsaShare<S>, S::Pseudonym> {
460 self.share.encrypt(&self.id, ack)
461 }
462}
463
464type ScalarRepr<S> = <PixScalar<S> as PrimeField>::Repr;
466
467#[derive(Debug)]
500#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
501#[cfg_attr(
502 feature = "serde",
503 serde(bound(
504 serialize = "PixGroupRepr<S>: serde::Serialize, ScalarRepr<S>: serde::Serialize",
505 deserialize = "PixGroupRepr<S>: serde::Deserialize<'de>, ScalarRepr<S>: serde::Deserialize<'de>"
506 ))
507)]
508pub struct SsaCommitmentProof<S: PixSpec> {
509 nonce_commitment: PixGroupRepr<S>,
511 response: ScalarRepr<S>,
513}
514
515impl<S: PixSpec> Clone for SsaCommitmentProof<S> {
518 fn clone(&self) -> Self {
519 *self
520 }
521}
522
523impl<S: PixSpec> Copy for SsaCommitmentProof<S> {}
524
525impl<S: PixSpec> PartialEq for SsaCommitmentProof<S> {
526 fn eq(&self, other: &Self) -> bool {
527 self.nonce_commitment.as_ref() == other.nonce_commitment.as_ref()
528 && AsRef::<[u8]>::as_ref(&self.response) == AsRef::<[u8]>::as_ref(&other.response)
529 }
530}
531
532impl<S: PixSpec> Eq for SsaCommitmentProof<S> {}
533
534impl<S: PixSpec> SsaCommitmentProof<S> {
535 pub const SIZE: usize = size_of::<PixGroupRepr<S>>() + size_of::<ScalarRepr<S>>();
537
538 pub fn prove(
540 ssa_id: &SsaId<S::Pseudonym>,
541 secret: &PixScalar<S>,
542 ssa_commitment: &PixGroup<S>,
543 ) -> errors::Result<Self, S::Pseudonym> {
544 let nonce =
548 <PixScalar<S> as crypto_traits::elliptic_curve::Field>::random(&mut hopr_types::crypto_random::rng());
549 let nonce_commitment = PixGroup::<S>::mul_by_generator(&nonce);
550 let challenge = S::commitment_proof_challenge(ssa_id, ssa_commitment, &nonce_commitment)?;
551
552 Ok(Self {
553 nonce_commitment: nonce_commitment.to_bytes(),
554 response: (nonce + challenge * *secret).to_repr(),
555 })
556 }
557
558 pub fn verify(&self, ssa_id: &SsaId<S::Pseudonym>, ssa_commitment: &PixGroup<S>) -> bool {
563 let Some(nonce_commitment) = Option::<PixGroup<S>>::from(PixGroup::<S>::from_bytes(&self.nonce_commitment))
564 else {
565 return false;
566 };
567 if !bool::from(crate::CofactorGroup::is_torsion_free(&nonce_commitment)) {
570 return false;
571 }
572 let Some(response) = Option::<PixScalar<S>>::from(PixScalar::<S>::from_repr(self.response)) else {
573 return false;
574 };
575 let Ok(challenge) = S::commitment_proof_challenge(ssa_id, ssa_commitment, &nonce_commitment) else {
576 return false;
577 };
578
579 PixGroup::<S>::mul_by_generator(&response) == nonce_commitment + *ssa_commitment * challenge
580 }
581
582 pub fn to_bytes(&self) -> Vec<u8> {
584 let mut out = Vec::with_capacity(Self::SIZE);
585 out.extend_from_slice(self.nonce_commitment.as_ref());
586 out.extend_from_slice(self.response.as_ref());
587 out
588 }
589
590 pub fn try_from_bytes(bytes: &[u8]) -> errors::Result<Self, S::Pseudonym> {
595 if bytes.len() != Self::SIZE {
596 return Err(PixError::InvalidInput);
597 }
598
599 let (nonce_bytes, response_bytes) = bytes.split_at(size_of::<PixGroupRepr<S>>());
600 let mut nonce_commitment = PixGroupRepr::<S>::default();
601 AsMut::<[u8]>::as_mut(&mut nonce_commitment).copy_from_slice(nonce_bytes);
602 let mut response = ScalarRepr::<S>::default();
603 AsMut::<[u8]>::as_mut(&mut response).copy_from_slice(response_bytes);
604
605 Ok(Self {
606 nonce_commitment,
607 response,
608 })
609 }
610}
611
612#[derive(Debug, Clone, PartialEq, Eq)]
614#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
615pub struct SsaCommitment<S: PixSpec, P = <S as PixSpec>::Pseudonym> {
616 pub ssa_id: SsaId<P>,
618 #[cfg_attr(feature = "serde", serde(with = "elliptic_curve_tools::group"))]
620 pub ssa_commitment: PixGroup<S>,
621 pub commitment_proof: SsaCommitmentProof<S>,
626 #[cfg_attr(
630 feature = "serde",
631 serde(bound(
632 serialize = "PixGroupRepr<S>: serde::Serialize",
633 deserialize = "PixGroupRepr<S>: serde::Deserialize<'de>"
634 ))
635 )]
636 pub verifiers: TransposedVerifiers<S>,
637}
638
639impl<S: PixSpec, P> IntoIterator for SsaCommitment<S, P> {
640 type IntoIter = std::collections::hash_map::IntoIter<CoefficientIndex, Vec<(PolynomialIndex, PixGroupRepr<S>)>>;
641 type Item = (CoefficientIndex, Vec<(PolynomialIndex, PixGroupRepr<S>)>);
642
643 fn into_iter(self) -> Self::IntoIter {
644 self.verifiers.into_iter()
645 }
646}
647
648impl<S: PixSpec> SsaCommitment<S, S::Pseudonym> {
649 pub fn reconstruct_part_commitments(self) -> errors::Result<Vec<SsaPartCommitment<S>>, S::Pseudonym> {
655 let constant_terms: BTreeMap<PolynomialIndex, PixGroupRepr<S>> = self
656 .verifiers
657 .get(&crate::CONSTANT_TERM_COEFFICIENT)
658 .into_iter()
659 .flatten()
660 .copied()
661 .collect();
662
663 constant_terms
664 .into_iter()
665 .map(|(poly_idx, commitment)| {
666 Ok(SsaPartCommitment::from_decoded_commitment(
667 SsaPolynomialId::new(self.ssa_id, poly_idx),
668 SsaPartCommitment::<S>::decode_commitment(&commitment)?,
669 ))
670 })
671 .collect()
672 }
673
674 pub fn process_into_reconstructor<R: ExitAcknowledgementShareProcessor<S>>(
680 self,
681 reconstructor: &R,
682 ) -> Result<(), R::Error> {
683 let ssa_id = self.ssa_id;
684 let proof = self.commitment_proof;
685 for (coeff_idx, coeffs) in self.verifiers {
686 let batch_proof = (coeff_idx == crate::CONSTANT_TERM_COEFFICIENT).then_some(proof);
687 reconstructor.insert_coefficient_commitments(ssa_id, coeff_idx, batch_proof, coeffs.into_iter())?;
688 }
689 Ok(())
690 }
691}
692
693#[derive(Debug, Clone, Copy, PartialEq, Eq)]
695#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
696pub struct SsaCommitmentState<P, A> {
697 pub ssa_id: SsaId<P>,
699 pub ssa_deposit_address: Option<A>,
701 pub is_verifiable: bool,
703 pub is_first_encountered: bool,
705 pub deposit_address_first_encountered: bool,
707}
708
709impl<P, A> SsaCommitmentState<P, A> {
710 pub fn new(ssa_id: SsaId<P>) -> Self {
714 Self {
715 ssa_id,
716 ssa_deposit_address: None,
717 is_verifiable: false,
718 is_first_encountered: true,
719 deposit_address_first_encountered: false,
720 }
721 }
722}
723
724#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
734pub struct SsaRecoveryProgress<P> {
735 pub ssa_id: SsaId<P>,
737 pub useful_shares: u64,
740 pub target_useful_shares: u64,
745 pub recovered_polynomials: u16,
747}
748
749#[derive(Clone, Copy)]
753pub struct RecoveredSsa<P, A> {
754 pub ssa_id: SsaId<P>,
756 pub ssa: A,
758}
759
760impl<P: std::fmt::Debug, A> std::fmt::Debug for RecoveredSsa<P, A> {
761 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
762 f.debug_struct("RecoveredSsa")
763 .field("ssa_id", &self.ssa_id)
764 .finish_non_exhaustive()
765 }
766}
767
768impl<P: PartialEq, A> PartialEq for RecoveredSsa<P, A> {
769 fn eq(&self, other: &Self) -> bool {
770 self.ssa_id == other.ssa_id
771 }
772}
773
774impl<P: Eq, A> Eq for RecoveredSsa<P, A> {}
775
776impl<P: std::hash::Hash, A> std::hash::Hash for RecoveredSsa<P, A> {
777 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
778 self.ssa_id.hash(state);
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use hopr_types::{crypto::types::SimplePseudonym, crypto_random::Randomizable};
785 use vsss_rs::elliptic_curve::Field;
786
787 use super::*;
788 use crate::tests::TestSpec;
789
790 #[test]
791 fn test_tagged_encrypted_partial_ssa_share_traits() {
792 let pseudonym = SimplePseudonym::random();
793 let nonce = 42u64;
794 let partial_share = EncryptedPartialSsaShare::<TestSpec>::default();
795 let tagged = TaggedEncryptedPartialSsaShare {
796 pseudonym,
797 nonce,
798 partial_share,
799 };
800
801 #[allow(clippy::clone_on_copy)]
803 let cloned = tagged.clone();
804 assert_eq!(tagged, cloned);
805
806 let copied = tagged;
808 assert_eq!(tagged, copied);
809
810 let debug_str = format!("{:?}", tagged);
812 assert!(debug_str.contains("TaggedEncryptedPartialSsaShare"));
813 assert!(debug_str.contains("pseudonym"));
814 assert!(debug_str.contains("nonce"));
815 assert!(debug_str.contains("partial_share"));
816 }
817
818 #[cfg(feature = "serde")]
819 #[test]
820 fn test_tagged_encrypted_partial_ssa_share_serde() {
821 let pseudonym = SimplePseudonym::random();
822 let nonce = 42u64;
823 let partial_share = EncryptedPartialSsaShare::<TestSpec>::default();
824 let _tagged = TaggedEncryptedPartialSsaShare {
825 pseudonym,
826 nonce,
827 partial_share,
828 };
829 }
831
832 #[test]
833 fn size_of_indices_must_match() {
834 let actual = size_of::<SsaIndex>() + size_of::<PolynomialIndex>();
835 let expected = SsaPolyIndexPrefixSize::USIZE;
836 assert_eq!(actual, expected);
837 }
838
839 #[test]
840 fn default_enc_share_is_empty() {
841 assert!(EncryptedPartialSsaShare::<TestSpec>::default().is_empty());
842 assert!(EncryptedPartialSsaShare::<TestSpec>::default().indices().is_none());
843 }
844
845 #[test]
846 fn ssa_part_shares_should_encrypt_and_decrypt() -> anyhow::Result<()> {
847 let key = HalfKey::random();
848 let spi = SsaPolynomialId::<SimplePseudonym>::new(SsaId::new(SimplePseudonym::random(), 1.try_into()?), 0);
849 let scalar = PixScalar::<TestSpec>::random(&mut hopr_types::crypto_random::rng());
850
851 let share_1 = PartialSsaShare::<TestSpec>(scalar.to_repr());
852 let share_2 = share_1.clone().encrypt(&spi, &key)?.decrypt(spi.pseudonym(), &key)?;
853
854 assert_eq!(share_1, share_2);
855 Ok(())
856 }
857
858 #[test]
859 fn debug_redaction_partial_ssa_share() {
860 let scalar = PixScalar::<TestSpec>::random(&mut hopr_types::crypto_random::rng());
861 let share = PartialSsaShare::<TestSpec>(scalar.to_repr());
862 let debug = format!("{:?}", share);
863 assert!(debug.contains("PartialSsaShare"));
864 assert_eq!(
866 debug, "PartialSsaShare { .. }",
867 "PartialSsaShare Debug must exactly match the redacted format"
868 );
869 }
870
871 #[test]
872 fn debug_redaction_generated_share() {
873 use crate::tests::TestSpec;
874
875 let scalar = PixScalar::<TestSpec>::random(&mut hopr_types::crypto_random::rng());
876 let share = PartialSsaShare::<TestSpec>(scalar.to_repr());
877 let id = SsaPolynomialId::new(
878 SsaId::new(
879 SimplePseudonym::try_from([0u8; 10].as_ref()).unwrap(),
880 1.try_into().unwrap(),
881 ),
882 0,
883 );
884 let generated = GeneratedShare { id, share };
885 let debug = format!("{:?}", generated);
886
887 assert!(debug.contains("GeneratedShare"));
889 assert!(debug.contains("id"));
890
891 assert_eq!(
893 debug,
894 format!("GeneratedShare {{ id: {:?}, .. }}", id),
895 "GeneratedShare Debug must expose only the id field"
896 );
897 }
898
899 #[test]
900 fn debug_redaction_recovered_ssa() {
901 use hopr_types::crypto::{keypairs::Keypair, prelude::ChainKeypair};
902
903 let pseudonym = SimplePseudonym::random();
904 let ssa_id = SsaId::new(pseudonym, 1.try_into().unwrap());
905 let dummy_key = ChainKeypair::random();
906 let recovered = RecoveredSsa { ssa_id, ssa: dummy_key };
907 let debug = format!("{:?}", recovered);
908
909 assert!(debug.contains("RecoveredSsa"));
911 assert!(debug.contains("ssa_id"));
912
913 assert_eq!(
915 debug,
916 format!("RecoveredSsa {{ ssa_id: {:?}, .. }}", ssa_id),
917 "RecoveredSsa Debug must expose only the ssa_id field"
918 );
919 }
920
921 #[test]
924 fn recovered_ssa_equality_and_hash_ignore_the_key() {
925 use std::hash::{Hash as _, Hasher as _};
926
927 use hopr_types::crypto::{keypairs::Keypair, prelude::ChainKeypair};
928
929 let ssa_id = SsaId::new(SimplePseudonym::random(), 1.try_into().unwrap());
930 let other_id = SsaId::new(SimplePseudonym::random(), 2.try_into().unwrap());
931
932 let hash_of = |r: &RecoveredSsa<SimplePseudonym, ChainKeypair>| {
933 let mut h = std::collections::hash_map::DefaultHasher::new();
934 r.hash(&mut h);
935 h.finish()
936 };
937
938 let a = RecoveredSsa {
939 ssa_id,
940 ssa: ChainKeypair::random(),
941 };
942 let b = RecoveredSsa {
943 ssa_id,
944 ssa: ChainKeypair::random(),
945 };
946 let c = RecoveredSsa {
947 ssa_id: other_id,
948 ssa: ChainKeypair::random(),
949 };
950
951 assert_eq!(a, b, "two different keys for the same SSA must compare equal");
952 assert_eq!(hash_of(&a), hash_of(&b), "and must hash equal");
953 assert_ne!(a, c);
954 assert_ne!(hash_of(&a), hash_of(&c));
955 }
956
957 #[test]
958 fn ssa_ids_expose_their_parts_and_display_hierarchically() {
959 let pseudonym = SimplePseudonym::random();
960 let ssa_index: SsaIndex = 7.try_into().unwrap();
961 let ssa_id = SsaId::new(pseudonym, ssa_index);
962
963 assert_eq!(ssa_id.pseudonym(), &pseudonym);
964 assert_eq!(ssa_id.ssa_index(), ssa_index);
965 assert_eq!(ssa_id.to_string(), format!("{pseudonym}-ssa#7"));
966
967 let spi = SsaPolynomialId::new(ssa_id, 3);
968 assert_eq!(spi.pseudonym(), &pseudonym);
969 assert_eq!(spi.ssa_index(), ssa_index);
970 assert_eq!(spi.poly_index(), 3);
971 assert_eq!(AsRef::<SsaId<_>>::as_ref(&spi), &ssa_id);
974 assert_eq!(spi.to_string(), format!("{ssa_id}:3"));
975 }
976
977 #[test]
978 fn encrypted_share_round_trips_through_bytes_and_rejects_the_wrong_length() {
979 let key = HalfKey::random();
980 let spi =
981 SsaPolynomialId::<SimplePseudonym>::new(SsaId::new(SimplePseudonym::random(), 5.try_into().unwrap()), 9);
982 let scalar = PixScalar::<TestSpec>::random(&mut hopr_types::crypto_random::rng());
983 let encrypted = PartialSsaShare::<TestSpec>(scalar.to_repr())
984 .encrypt(&spi, &key)
985 .expect("encryption must succeed");
986
987 assert_eq!(encrypted.indices(), Some((spi.ssa_index(), spi.poly_index())));
990 assert!(!encrypted.is_empty());
991
992 let bytes: &[u8] = encrypted.as_ref();
993 assert_eq!(bytes.len(), EncryptedPartialSsaShare::<TestSpec>::SIZE);
994 assert_eq!(
995 EncryptedPartialSsaShare::<TestSpec>::try_from(bytes).expect("exact length must parse"),
996 encrypted
997 );
998
999 for wrong in [&bytes[..bytes.len() - 1], &[][..]] {
1000 assert!(
1001 EncryptedPartialSsaShare::<TestSpec>::try_from(wrong).is_err(),
1002 "a {}-byte buffer must not parse as a {}-byte share",
1003 wrong.len(),
1004 EncryptedPartialSsaShare::<TestSpec>::SIZE
1005 );
1006 }
1007 }
1008
1009 #[cfg(feature = "serde")]
1010 #[test]
1011 fn encrypted_share_serde_round_trips_as_opaque_bytes() {
1012 let key = HalfKey::random();
1013 let spi =
1014 SsaPolynomialId::<SimplePseudonym>::new(SsaId::new(SimplePseudonym::random(), 2.try_into().unwrap()), 1);
1015 let scalar = PixScalar::<TestSpec>::random(&mut hopr_types::crypto_random::rng());
1016 let encrypted = PartialSsaShare::<TestSpec>(scalar.to_repr())
1017 .encrypt(&spi, &key)
1018 .expect("encryption must succeed");
1019
1020 let encoded = serde_cbor_2::to_vec(&encrypted).expect("serialization must succeed");
1023 let decoded: EncryptedPartialSsaShare<TestSpec> =
1024 serde_cbor_2::from_slice(&encoded).expect("deserialization must succeed");
1025 assert_eq!(decoded, encrypted);
1026
1027 let truncated = [0x43u8, 0x00, 0x00, 0x00];
1030 assert!(serde_cbor_2::from_slice::<EncryptedPartialSsaShare<TestSpec>>(&truncated).is_err());
1031 }
1032
1033 #[test]
1034 fn commitment_proof_verifies_only_against_the_commitment_it_opens() -> anyhow::Result<()> {
1035 let ssa_id = SsaId::new(SimplePseudonym::random(), 1.try_into()?);
1036 let secret = PixScalar::<TestSpec>::random(&mut hopr_types::crypto_random::rng());
1037 let commitment = PixGroup::<TestSpec>::mul_by_generator(&secret);
1038
1039 let proof = SsaCommitmentProof::<TestSpec>::prove(&ssa_id, &secret, &commitment)?;
1040 assert!(proof.verify(&ssa_id, &commitment));
1041
1042 let other_id = SsaId::new(*ssa_id.pseudonym(), 2.try_into()?);
1044 assert!(!proof.verify(&other_id, &commitment), "the proof must bind the SSA id");
1045
1046 let other_secret = PixScalar::<TestSpec>::random(&mut hopr_types::crypto_random::rng());
1048 let other_commitment = PixGroup::<TestSpec>::mul_by_generator(&other_secret);
1049 assert!(
1050 !proof.verify(&ssa_id, &other_commitment),
1051 "the proof must bind the commitment"
1052 );
1053
1054 let again = SsaCommitmentProof::<TestSpec>::prove(&ssa_id, &secret, &commitment)?;
1057 assert_ne!(proof, again, "the proof nonce must be fresh for every proof");
1058 assert!(again.verify(&ssa_id, &commitment));
1059
1060 #[allow(clippy::clone_on_copy)]
1062 let cloned = proof.clone();
1063 assert_eq!(proof, cloned);
1064
1065 Ok(())
1066 }
1067
1068 #[test]
1069 fn commitment_proof_round_trips_through_bytes() -> anyhow::Result<()> {
1070 let ssa_id = SsaId::new(SimplePseudonym::random(), 1.try_into()?);
1071 let secret = PixScalar::<TestSpec>::random(&mut hopr_types::crypto_random::rng());
1072 let commitment = PixGroup::<TestSpec>::mul_by_generator(&secret);
1073 let proof = SsaCommitmentProof::<TestSpec>::prove(&ssa_id, &secret, &commitment)?;
1074
1075 let bytes = proof.to_bytes();
1076 assert_eq!(bytes.len(), SsaCommitmentProof::<TestSpec>::SIZE);
1077
1078 let parsed = SsaCommitmentProof::<TestSpec>::try_from_bytes(&bytes)?;
1079 assert_eq!(parsed, proof);
1080 assert!(parsed.verify(&ssa_id, &commitment));
1081
1082 for wrong in [&bytes[..bytes.len() - 1], &[][..]] {
1083 assert!(
1084 SsaCommitmentProof::<TestSpec>::try_from_bytes(wrong).is_err(),
1085 "only the exact length may parse"
1086 );
1087 }
1088
1089 let garbage = SsaCommitmentProof::<TestSpec>::try_from_bytes(&[0xAA; SsaCommitmentProof::<TestSpec>::SIZE])?;
1092 assert!(!garbage.verify(&ssa_id, &commitment));
1093
1094 Ok(())
1095 }
1096
1097 #[test]
1098 fn ssa_commitment_iterates_its_transposed_verifiers() -> anyhow::Result<()> {
1099 let ssa_id = SsaId::new(SimplePseudonym::random(), 1.try_into()?);
1100 let secret = PixScalar::<TestSpec>::random(&mut hopr_types::crypto_random::rng());
1101 let ssa_commitment = PixGroup::<TestSpec>::mul_by_generator(&secret);
1102 let commitment_proof = SsaCommitmentProof::<TestSpec>::prove(&ssa_id, &secret, &ssa_commitment)?;
1103
1104 let entries: Vec<(PolynomialIndex, PixGroupRepr<TestSpec>)> = (0..3u16)
1105 .map(|poly_index| {
1106 let s = PixScalar::<TestSpec>::random(&mut hopr_types::crypto_random::rng());
1107 (poly_index, PixGroup::<TestSpec>::mul_by_generator(&s).to_bytes())
1108 })
1109 .collect();
1110
1111 let commitment = SsaCommitment::<TestSpec> {
1112 ssa_id,
1113 ssa_commitment,
1114 commitment_proof,
1115 verifiers: HashMap::from([(crate::CONSTANT_TERM_COEFFICIENT, entries.clone())]),
1116 };
1117
1118 let collected: Vec<_> = commitment.into_iter().collect();
1119 assert_eq!(collected.len(), 1, "PIX commits to the constant term and nothing else");
1120 assert_eq!(collected[0].0, crate::CONSTANT_TERM_COEFFICIENT);
1121 assert_eq!(collected[0].1, entries);
1122
1123 Ok(())
1124 }
1125
1126 #[test]
1127 fn ssa_commitment_state_starts_unverifiable_and_unaddressed() {
1128 let ssa_id = SsaId::new(SimplePseudonym::random(), 1.try_into().unwrap());
1129 let state = SsaCommitmentState::<SimplePseudonym, hopr_types::primitive::prelude::Address>::new(ssa_id);
1130
1131 assert_eq!(state.ssa_id, ssa_id);
1132 assert!(
1133 state.ssa_deposit_address.is_none(),
1134 "the address is the sum of constant terms, none of which have arrived yet"
1135 );
1136 assert!(!state.is_verifiable, "shares cannot verify before the commitment does");
1137 assert!(state.is_first_encountered);
1138 assert!(!state.deposit_address_first_encountered);
1139 }
1140}