1use std::{
2 cmp::Ordering,
3 fmt::{Display, Formatter},
4};
5
6use hex_literal::hex;
7use hopr_crypto_types::prelude::*;
8use hopr_primitive_types::prelude::*;
9use tracing::{debug, error};
10
11use crate::{
12 errors,
13 errors::CoreTypesError,
14 prelude::{CoreTypesError::InvalidInputData, generate_channel_id},
15};
16
17const ENCODED_TICKET_LENGTH: usize = 64;
20
21const ENCODED_WIN_PROB_LENGTH: usize = 7;
25
26pub const REDEEM_CALL_SELECTOR: [u8; 4] = [252, 183, 121, 111];
30
31pub type EncodedWinProb = [u8; ENCODED_WIN_PROB_LENGTH];
33
34#[derive(Clone, Copy, Debug)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub struct WinningProbability(#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] EncodedWinProb);
42
43impl WinningProbability {
44 pub const ALWAYS: Self = Self([0xff; ENCODED_WIN_PROB_LENGTH]);
46 pub const EPSILON: f64 = 0.00000001;
49 pub const NEVER: Self = Self([0u8; ENCODED_WIN_PROB_LENGTH]);
51
52 pub fn as_luck(&self) -> u64 {
54 let mut tmp = [0u8; 8];
55 tmp[1..].copy_from_slice(&self.0);
56 u64::from_be_bytes(tmp)
57 }
58
59 pub fn as_encoded(&self) -> EncodedWinProb {
61 self.0
62 }
63
64 pub fn as_f64(&self) -> f64 {
66 if self.0.eq(&Self::NEVER.0) {
67 return 0.0;
68 }
69
70 if self.0.eq(&Self::ALWAYS.0) {
71 return 1.0;
72 }
73
74 let mut tmp = [0u8; 8];
75 tmp[1..].copy_from_slice(&self.0);
76
77 let tmp = u64::from_be_bytes(tmp);
78
79 let significand: u64 = tmp + 1;
81
82 f64::from_bits((1023u64 << 52) | (significand >> 4)) - 1.0
83 }
84
85 pub fn try_from_f64(win_prob: f64) -> errors::Result<Self> {
87 if !(0.0..=1.0).contains(&win_prob) {
89 return Err(InvalidInputData("winning probability must be in [0.0, 1.0]".into()));
90 }
91
92 if f64_approx_eq(0.0, win_prob, Self::EPSILON) {
93 return Ok(Self::NEVER);
94 }
95
96 if f64_approx_eq(1.0, win_prob, Self::EPSILON) {
97 return Ok(Self::ALWAYS);
98 }
99
100 let tmp: u64 = (win_prob + 1.0).to_bits();
101
102 let significand: u64 = tmp & 0x000fffffffffffffu64;
104
105 let encoded = ((significand - 1) << 4) | 0x000000000000000fu64;
107
108 let mut res = [0u8; 7];
109 res.copy_from_slice(&encoded.to_be_bytes()[1..]);
110
111 Ok(Self(res))
112 }
113
114 pub fn approx_cmp(&self, other: &Self) -> Ordering {
116 let a = self.as_f64();
117 let b = other.as_f64();
118 if !f64_approx_eq(a, b, Self::EPSILON) {
119 a.partial_cmp(&b).expect("finite non-NaN f64 comparison cannot fail")
120 } else {
121 Ordering::Equal
122 }
123 }
124
125 pub fn approx_eq(&self, other: &Self) -> bool {
127 self.approx_cmp(other) == Ordering::Equal
128 }
129
130 pub fn min(&self, other: &Self) -> Self {
132 if self.approx_cmp(other) == Ordering::Less {
133 *self
134 } else {
135 *other
136 }
137 }
138
139 pub fn max(&self, other: &Self) -> Self {
141 if self.approx_cmp(other) == Ordering::Greater {
142 *self
143 } else {
144 *other
145 }
146 }
147}
148
149impl Default for WinningProbability {
150 fn default() -> Self {
151 Self::ALWAYS
152 }
153}
154
155impl Display for WinningProbability {
156 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
157 write!(f, "{:.8}", self.as_f64())
158 }
159}
160
161impl From<EncodedWinProb> for WinningProbability {
162 fn from(value: EncodedWinProb) -> Self {
163 Self(value)
164 }
165}
166
167impl<'a> From<&'a EncodedWinProb> for WinningProbability {
168 fn from(value: &'a EncodedWinProb) -> Self {
169 Self(*value)
170 }
171}
172
173impl From<WinningProbability> for EncodedWinProb {
174 fn from(value: WinningProbability) -> Self {
175 value.0
176 }
177}
178
179impl From<u64> for WinningProbability {
180 fn from(value: u64) -> Self {
181 let mut ret = Self::default();
182 ret.0.copy_from_slice(&value.to_be_bytes()[1..]);
183 ret
184 }
185}
186
187impl TryFrom<f64> for WinningProbability {
188 type Error = CoreTypesError;
189
190 fn try_from(value: f64) -> Result<Self, Self::Error> {
191 Self::try_from_f64(value)
192 }
193}
194
195impl From<WinningProbability> for f64 {
196 fn from(value: WinningProbability) -> Self {
197 value.as_f64()
198 }
199}
200
201impl PartialEq<f64> for WinningProbability {
202 fn eq(&self, other: &f64) -> bool {
203 f64_approx_eq(self.as_f64(), *other, Self::EPSILON)
204 }
205}
206
207impl PartialEq<WinningProbability> for f64 {
208 fn eq(&self, other: &WinningProbability) -> bool {
209 f64_approx_eq(*self, other.as_f64(), WinningProbability::EPSILON)
210 }
211}
212
213impl AsRef<[u8]> for WinningProbability {
214 fn as_ref(&self) -> &[u8] {
215 &self.0
216 }
217}
218
219impl<'a> TryFrom<&'a [u8]> for WinningProbability {
220 type Error = GeneralError;
221
222 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
223 value
224 .try_into()
225 .map(Self)
226 .map_err(|_| GeneralError::ParseError("WinningProbability".into()))
227 }
228}
229
230impl BytesRepresentable for WinningProbability {
231 const SIZE: usize = ENCODED_WIN_PROB_LENGTH;
232}
233
234pub(crate) fn check_ticket_win(
236 ticket_hash: &Hash,
237 ticket_signature: &Signature,
238 win_prob: &WinningProbability,
239 response: &Response,
240 vrf_params: &VrfParameters,
241) -> bool {
242 let mut computed_ticket_luck = [0u8; 8];
244 computed_ticket_luck[1..].copy_from_slice(
245 &Hash::create(&[
246 ticket_hash.as_ref(),
247 &vrf_params.V.as_uncompressed().as_bytes()[1..], response.as_ref(),
249 ticket_signature.as_ref(),
250 ])
251 .as_ref()[0..7],
252 );
253
254 u64::from_be_bytes(computed_ticket_luck) <= win_prob.as_luck()
255}
256
257#[derive(Debug, Clone, smart_default::SmartDefault)]
264pub struct TicketBuilder {
265 channel_id: Option<Hash>,
266 amount: Option<U256>,
267 balance: Option<HoprBalance>,
268 #[default = 0]
269 index: u64,
270 #[default = 1]
271 index_offset: u32,
272 #[default = 1]
273 channel_epoch: u32,
274 win_prob: WinningProbability,
275 challenge: Option<EthereumChallenge>,
276 signature: Option<Signature>,
277}
278
279impl TicketBuilder {
280 #[must_use]
282 pub fn zero_hop() -> Self {
283 Self {
284 index: 0,
285 amount: Some(U256::zero()),
286 index_offset: 1,
287 win_prob: WinningProbability::NEVER,
288 channel_epoch: 0,
289 ..Default::default()
290 }
291 }
292
293 #[must_use]
296 pub fn direction(mut self, source: &Address, destination: &Address) -> Self {
297 self.channel_id = Some(generate_channel_id(source, destination));
298 self
299 }
300
301 #[must_use]
304 pub fn addresses<T: Into<Address>, U: Into<Address>>(mut self, source: T, destination: U) -> Self {
305 self.channel_id = Some(generate_channel_id(&source.into(), &destination.into()));
306 self
307 }
308
309 #[must_use]
312 pub fn channel_id(mut self, channel_id: Hash) -> Self {
313 self.channel_id = Some(channel_id);
314 self
315 }
316
317 #[must_use]
320 pub fn amount<T: Into<U256>>(mut self, amount: T) -> Self {
321 self.amount = Some(amount.into());
322 self.balance = None;
323 self
324 }
325
326 #[must_use]
329 pub fn balance(mut self, balance: HoprBalance) -> Self {
330 self.balance = Some(balance);
331 self.amount = None;
332 self
333 }
334
335 #[must_use]
339 pub fn index(mut self, index: u64) -> Self {
340 self.index = index;
341 self
342 }
343
344 #[must_use]
348 pub fn index_offset(mut self, index_offset: u32) -> Self {
349 self.index_offset = index_offset;
350 self
351 }
352
353 #[must_use]
357 pub fn channel_epoch(mut self, channel_epoch: u32) -> Self {
358 self.channel_epoch = channel_epoch;
359 self
360 }
361
362 #[must_use]
365 pub fn win_prob(mut self, win_prob: WinningProbability) -> Self {
366 self.win_prob = win_prob;
367 self
368 }
369
370 #[must_use]
373 pub fn challenge(mut self, challenge: EthereumChallenge) -> Self {
374 self.challenge = Some(challenge);
375 self
376 }
377
378 #[must_use]
381 pub fn signature(mut self, signature: Signature) -> Self {
382 self.signature = Some(signature);
383 self
384 }
385
386 pub fn build(self) -> errors::Result<Ticket> {
390 let amount = match (self.amount, self.balance) {
391 (Some(amount), None) if amount.lt(&10_u128.pow(25).into()) => HoprBalance::from(amount),
392 (None, Some(balance)) if balance.amount().lt(&10_u128.pow(25).into()) => balance,
393 (None, None) => return Err(InvalidInputData("missing ticket amount".into())),
394 (Some(_), Some(_)) => {
395 return Err(InvalidInputData(
396 "either amount or balance must be set but not both".into(),
397 ));
398 }
399 _ => {
400 return Err(InvalidInputData(
401 "tickets may not have more than 1% of total supply".into(),
402 ));
403 }
404 };
405
406 if self.index > (1_u64 << 48) {
407 return Err(InvalidInputData("cannot hold ticket indices larger than 2^48".into()));
408 }
409
410 if self.channel_epoch > (1_u32 << 24) {
411 return Err(InvalidInputData("cannot hold channel epoch larger than 2^24".into()));
412 }
413
414 if self.index_offset < 1 {
415 return Err(InvalidInputData(
416 "ticket index offset must be greater or equal to 1".into(),
417 ));
418 }
419
420 Ok(Ticket {
421 channel_id: self.channel_id.ok_or(InvalidInputData("missing channel id".into()))?,
422 amount,
423 index: self.index,
424 index_offset: self.index_offset,
425 encoded_win_prob: self.win_prob.into(),
426 channel_epoch: self.channel_epoch,
427 challenge: self
428 .challenge
429 .ok_or(InvalidInputData("missing ticket challenge".into()))?,
430 signature: self.signature,
431 })
432 }
433
434 pub fn build_signed(self, signer: &ChainKeypair, domain_separator: &Hash) -> errors::Result<VerifiedTicket> {
437 if self.signature.is_none() {
438 Ok(self.build()?.sign(signer, domain_separator))
439 } else {
440 Err(InvalidInputData("signature already set".into()))
441 }
442 }
443
444 pub fn build_verified(self, hash: Hash) -> errors::Result<VerifiedTicket> {
449 if let Some(signature) = self.signature {
450 let issuer = PublicKey::from_signature_hash(hash.as_ref(), &signature)?.to_address();
451 Ok(VerifiedTicket(self.build()?, hash, issuer))
452 } else {
453 Err(InvalidInputData("signature is missing".into()))
454 }
455 }
456}
457
458impl From<&Ticket> for TicketBuilder {
459 fn from(value: &Ticket) -> Self {
460 Self {
461 channel_id: Some(value.channel_id),
462 amount: None,
463 balance: Some(value.amount),
464 index: value.index,
465 index_offset: value.index_offset,
466 channel_epoch: value.channel_epoch,
467 win_prob: value.encoded_win_prob.into(),
468 challenge: Some(value.challenge),
469 signature: None,
470 }
471 }
472}
473
474impl From<Ticket> for TicketBuilder {
475 fn from(value: Ticket) -> Self {
476 Self::from(&value)
477 }
478}
479
480#[cfg_attr(doc, aquamarine::aquamarine)]
481#[derive(Clone, Debug, PartialEq, Eq)]
502#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
503pub struct Ticket {
504 pub channel_id: Hash,
507 pub amount: HoprBalance, pub index: u64, pub index_offset: u32, pub encoded_win_prob: EncodedWinProb, pub channel_epoch: u32, pub challenge: EthereumChallenge,
524 pub signature: Option<Signature>,
526}
527
528impl PartialOrd for Ticket {
529 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
530 Some(self.cmp(other))
531 }
532}
533
534impl Ord for Ticket {
535 fn cmp(&self, other: &Self) -> Ordering {
536 match self.channel_id.cmp(&other.channel_id) {
539 Ordering::Equal => match self.channel_epoch.cmp(&other.channel_epoch) {
540 Ordering::Equal => self.index.cmp(&other.index),
541 Ordering::Greater => Ordering::Greater,
542 Ordering::Less => Ordering::Less,
543 },
544 Ordering::Greater => Ordering::Greater,
545 Ordering::Less => Ordering::Less,
546 }
547 }
548}
549
550impl Display for Ticket {
551 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
552 write!(
553 f,
554 "ticket #{}, amount {}, offset {}, epoch {} in channel {}",
555 self.index, self.amount, self.index_offset, self.channel_epoch, self.channel_id
556 )
557 }
558}
559
560impl Ticket {
561 fn encode_without_signature(&self) -> [u8; Self::SIZE - Signature::SIZE] {
562 let mut ret = [0u8; Self::SIZE - Signature::SIZE];
563 let mut offset = 0;
564
565 ret[offset..offset + Hash::SIZE].copy_from_slice(self.channel_id.as_ref());
566 offset += Hash::SIZE;
567
568 ret[offset..offset + 12].copy_from_slice(&self.amount.amount().to_be_bytes()[20..32]);
570 offset += 12;
571
572 ret[offset..offset + 6].copy_from_slice(&self.index.to_be_bytes()[2..8]);
574 offset += 6;
575
576 ret[offset..offset + 4].copy_from_slice(&self.index_offset.to_be_bytes());
577 offset += 4;
578
579 ret[offset..offset + 3].copy_from_slice(&self.channel_epoch.to_be_bytes()[1..4]);
581 offset += 3;
582
583 ret[offset..offset + ENCODED_WIN_PROB_LENGTH].copy_from_slice(&self.encoded_win_prob);
584 offset += ENCODED_WIN_PROB_LENGTH;
585
586 ret[offset..offset + EthereumChallenge::SIZE].copy_from_slice(self.challenge.as_ref());
587
588 ret
589 }
590
591 pub fn get_hash(&self, domain_separator: &Hash) -> Hash {
594 let ticket_hash = Hash::create(&[self.encode_without_signature().as_ref()]); let hash_struct = Hash::create(&[&REDEEM_CALL_SELECTOR, &[0u8; 28], ticket_hash.as_ref()]);
596 Hash::create(&[&hex!("1901"), domain_separator.as_ref(), hash_struct.as_ref()])
597 }
598
599 pub fn sign(mut self, signing_key: &ChainKeypair, domain_separator: &Hash) -> VerifiedTicket {
602 let ticket_hash = self.get_hash(domain_separator);
603 self.signature = Some(Signature::sign_hash(ticket_hash.as_ref(), signing_key));
604 VerifiedTicket(self, ticket_hash, signing_key.public().to_address())
605 }
606
607 pub fn verify(self, issuer: &Address, domain_separator: &Hash) -> Result<VerifiedTicket, Box<Ticket>> {
615 let ticket_hash = self.get_hash(domain_separator);
616
617 if let Some(signature) = &self.signature {
618 match PublicKey::from_signature_hash(ticket_hash.as_ref(), signature) {
619 Ok(pk) if pk.to_address().eq(issuer) => Ok(VerifiedTicket(self, ticket_hash, *issuer)),
620 Err(e) => {
621 error!("failed to verify ticket signature: {e}");
622 Err(self.into())
623 }
624 _ => Err(self.into()),
625 }
626 } else {
627 Err(self.into())
628 }
629 }
630
631 pub fn is_aggregated(&self) -> bool {
633 self.index_offset > 1
635 }
636
637 pub fn win_prob(&self) -> WinningProbability {
639 WinningProbability(self.encoded_win_prob)
640 }
641}
642
643impl From<Ticket> for [u8; TICKET_SIZE] {
644 fn from(value: Ticket) -> Self {
645 let mut ret = [0u8; TICKET_SIZE];
646 ret[0..Ticket::SIZE - Signature::SIZE].copy_from_slice(value.encode_without_signature().as_ref());
647 ret[Ticket::SIZE - Signature::SIZE..].copy_from_slice(
648 value
649 .signature
650 .expect("cannot serialize ticket without signature")
651 .as_ref(),
652 );
653 ret
654 }
655}
656
657impl TryFrom<&[u8]> for Ticket {
658 type Error = GeneralError;
659
660 fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
661 if value.len() == Self::SIZE {
662 let mut offset = 0;
663
664 let channel_id = Hash::try_from(&value[offset..offset + Hash::SIZE])?;
666 offset += Hash::SIZE;
667
668 let mut amount = [0u8; 32];
669 amount[20..32].copy_from_slice(&value[offset..offset + 12]);
670 offset += 12;
671
672 let mut index = [0u8; 8];
673 index[2..8].copy_from_slice(&value[offset..offset + 6]);
674 offset += 6;
675
676 let mut index_offset = [0u8; 4];
677 index_offset.copy_from_slice(&value[offset..offset + 4]);
678 offset += 4;
679
680 let mut channel_epoch = [0u8; 4];
681 channel_epoch[1..4].copy_from_slice(&value[offset..offset + 3]);
682 offset += 3;
683
684 let win_prob = WinningProbability::try_from(&value[offset..offset + WinningProbability::SIZE])?;
685 offset += WinningProbability::SIZE;
686
687 debug_assert_eq!(offset, ENCODED_TICKET_LENGTH);
688
689 let challenge = EthereumChallenge::try_from(&value[offset..offset + EthereumChallenge::SIZE])?;
690 offset += EthereumChallenge::SIZE;
691
692 let signature = Signature::try_from(&value[offset..offset + Signature::SIZE])?;
693
694 TicketBuilder::default()
696 .channel_id(channel_id)
697 .amount(U256::from_big_endian(&amount))
698 .index(u64::from_be_bytes(index))
699 .index_offset(u32::from_be_bytes(index_offset))
700 .channel_epoch(u32::from_be_bytes(channel_epoch))
701 .win_prob(win_prob)
702 .challenge(challenge)
703 .signature(signature)
704 .build()
705 .map_err(|e| GeneralError::ParseError(format!("ticket build failed: {e}")))
706 } else {
707 Err(GeneralError::ParseError("Ticket".into()))
708 }
709 }
710}
711
712const TICKET_SIZE: usize = ENCODED_TICKET_LENGTH + EthereumChallenge::SIZE + Signature::SIZE;
713
714impl BytesEncodable<TICKET_SIZE> for Ticket {}
715
716#[derive(Debug, Clone, PartialEq, Eq)]
720#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
721pub struct VerifiedTicket(Ticket, Hash, Address);
722
723impl VerifiedTicket {
724 pub fn win_prob(&self) -> WinningProbability {
726 self.0.win_prob()
727 }
728
729 pub fn is_winning(&self, response: &Response, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
747 if let Ok(vrf_params) = derive_vrf_parameters(self.1, chain_keypair, domain_separator.as_ref()) {
748 check_ticket_win(
749 &self.1,
750 self.0
751 .signature
752 .as_ref()
753 .expect("verified ticket have always a signature"),
754 &self.0.win_prob(),
755 response,
756 &vrf_params,
757 )
758 } else {
759 error!("cannot derive vrf parameters for {self}");
760 false
761 }
762 }
763
764 pub fn verified_ticket(&self) -> &Ticket {
766 &self.0
767 }
768
769 pub fn verified_hash(&self) -> &Hash {
772 &self.1
773 }
774
775 pub fn verified_issuer(&self) -> &Address {
779 &self.2
780 }
781
782 pub fn verified_signature(&self) -> &Signature {
784 self.0
785 .signature
786 .as_ref()
787 .expect("verified ticket always has a signature")
788 }
789
790 pub fn leak(self) -> Ticket {
792 self.0
793 }
794
795 pub fn into_unacknowledged(self, own_key: HalfKey) -> UnacknowledgedTicket {
798 UnacknowledgedTicket { ticket: self, own_key }
799 }
800
801 pub fn into_acknowledged(self, response: Response) -> AcknowledgedTicket {
804 AcknowledgedTicket {
805 status: AcknowledgedTicketStatus::Untouched,
806 ticket: self,
807 response,
808 }
809 }
810}
811
812impl Display for VerifiedTicket {
813 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
814 write!(f, "verified {}", self.0)
815 }
816}
817
818impl PartialOrd for VerifiedTicket {
819 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
820 Some(self.cmp(other))
821 }
822}
823
824impl Ord for VerifiedTicket {
825 fn cmp(&self, other: &Self) -> Ordering {
826 self.0.cmp(&other.0)
827 }
828}
829
830#[derive(Clone, Debug, PartialEq, Eq)]
834#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
835pub struct UnacknowledgedTicket {
836 pub ticket: VerifiedTicket,
837 pub(crate) own_key: HalfKey,
838}
839
840impl UnacknowledgedTicket {
841 #[inline]
843 pub fn verified_ticket(&self) -> &Ticket {
844 self.ticket.verified_ticket()
845 }
846
847 pub fn acknowledge(self, acknowledgement: &HalfKey) -> crate::errors::Result<AcknowledgedTicket> {
851 let response = Response::from_half_keys(&self.own_key, acknowledgement)?;
852 debug!(ticket = %self.ticket, response = response.to_hex(), "acknowledging ticket using response");
853
854 if self.ticket.verified_ticket().challenge == response.to_challenge().into() {
855 Ok(self.ticket.into_acknowledged(response))
856 } else {
857 Err(CryptoError::InvalidChallenge.into())
858 }
859 }
860}
861
862#[repr(u8)]
864#[derive(
865 Clone,
866 Copy,
867 Debug,
868 Default,
869 Eq,
870 PartialEq,
871 strum::Display,
872 strum::EnumString,
873 num_enum::IntoPrimitive,
874 num_enum::TryFromPrimitive,
875)]
876#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
877#[strum(serialize_all = "PascalCase")]
878pub enum AcknowledgedTicketStatus {
879 #[default]
881 Untouched = 0,
882 BeingRedeemed = 1,
884 BeingAggregated = 2,
886}
887
888#[derive(Clone, Debug, PartialEq, Eq)]
890#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
891pub struct AcknowledgedTicket {
892 #[cfg_attr(feature = "serde", serde(default))]
893 pub status: AcknowledgedTicketStatus,
894 pub ticket: VerifiedTicket,
895 pub response: Response,
896}
897
898impl PartialOrd for AcknowledgedTicket {
899 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
900 Some(self.cmp(other))
901 }
902}
903
904impl Ord for AcknowledgedTicket {
905 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
906 self.ticket.cmp(&other.ticket)
907 }
908}
909
910impl AcknowledgedTicket {
911 #[inline]
913 pub fn verified_ticket(&self) -> &Ticket {
914 self.ticket.verified_ticket()
915 }
916
917 pub fn is_winning(&self, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
919 self.ticket.is_winning(&self.response, chain_keypair, domain_separator)
920 }
921
922 pub fn into_redeemable(
926 self,
927 chain_keypair: &ChainKeypair,
928 domain_separator: &Hash,
929 ) -> crate::errors::Result<RedeemableTicket> {
930 if chain_keypair.public().to_address().eq(self.ticket.verified_issuer()) {
932 return Err(errors::CoreTypesError::LoopbackTicket);
933 }
934
935 let vrf_params = derive_vrf_parameters(self.ticket.verified_hash(), chain_keypair, domain_separator.as_ref())?;
936
937 Ok(RedeemableTicket {
938 ticket: self.ticket,
939 response: self.response,
940 vrf_params,
941 channel_dst: *domain_separator,
942 })
943 }
944
945 pub fn into_transferable(
948 self,
949 chain_keypair: &ChainKeypair,
950 domain_separator: &Hash,
951 ) -> errors::Result<TransferableWinningTicket> {
952 self.into_redeemable(chain_keypair, domain_separator)
953 .map(TransferableWinningTicket::from)
954 }
955}
956
957impl Display for AcknowledgedTicket {
958 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
959 write!(f, "acknowledged {} in state '{}'", self.ticket, self.status)
960 }
961}
962
963#[derive(Clone, Debug)]
965#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
966pub struct RedeemableTicket {
967 pub ticket: VerifiedTicket,
969 pub response: Response,
971 pub vrf_params: VrfParameters,
973 pub channel_dst: Hash,
975}
976
977impl RedeemableTicket {
978 #[inline]
980 pub fn verified_ticket(&self) -> &Ticket {
981 self.ticket.verified_ticket()
982 }
983}
984
985impl PartialEq for RedeemableTicket {
986 fn eq(&self, other: &Self) -> bool {
987 self.ticket == other.ticket && self.channel_dst == other.channel_dst && self.response == other.response
988 }
989}
990
991impl Display for RedeemableTicket {
992 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
993 write!(f, "redeemable {}", self.ticket)
994 }
995}
996
997impl From<RedeemableTicket> for AcknowledgedTicket {
998 fn from(value: RedeemableTicket) -> Self {
999 Self {
1000 status: AcknowledgedTicketStatus::Untouched,
1001 ticket: value.ticket,
1002 response: value.response,
1003 }
1004 }
1005}
1006
1007#[derive(Debug, Clone)]
1015#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1016pub struct TransferableWinningTicket {
1017 pub ticket: Ticket,
1018 pub response: Response,
1019 pub vrf_params: VrfParameters,
1020 pub signer: Address,
1021}
1022
1023impl TransferableWinningTicket {
1024 pub fn into_redeemable(
1031 self,
1032 expected_issuer: &Address,
1033 domain_separator: &Hash,
1034 ) -> errors::Result<RedeemableTicket> {
1035 if !self.signer.eq(expected_issuer) {
1036 return Err(crate::errors::CoreTypesError::InvalidInputData(
1037 "invalid ticket issuer".into(),
1038 ));
1039 }
1040
1041 let verified_ticket = self
1042 .ticket
1043 .verify(&self.signer, domain_separator)
1044 .map_err(|_| CoreTypesError::CryptoError(CryptoError::SignatureVerification))?;
1045
1046 if check_ticket_win(
1047 verified_ticket.verified_hash(),
1048 verified_ticket.verified_signature(),
1049 &verified_ticket.verified_ticket().win_prob(),
1050 &self.response,
1051 &self.vrf_params,
1052 ) {
1053 Ok(RedeemableTicket {
1054 ticket: verified_ticket,
1055 response: self.response,
1056 vrf_params: self.vrf_params,
1057 channel_dst: *domain_separator,
1058 })
1059 } else {
1060 Err(crate::errors::CoreTypesError::InvalidInputData(
1061 "ticket is not a win".into(),
1062 ))
1063 }
1064 }
1065}
1066
1067impl PartialEq for TransferableWinningTicket {
1068 fn eq(&self, other: &Self) -> bool {
1069 self.ticket == other.ticket && self.signer == other.signer && self.response == other.response
1070 }
1071}
1072
1073impl PartialOrd<Self> for TransferableWinningTicket {
1074 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1075 Some(self.ticket.cmp(&other.ticket))
1076 }
1077}
1078
1079impl From<RedeemableTicket> for TransferableWinningTicket {
1080 fn from(value: RedeemableTicket) -> Self {
1081 Self {
1082 response: value.response,
1083 vrf_params: value.vrf_params,
1084 signer: *value.ticket.verified_issuer(),
1085 ticket: value.ticket.leak(),
1086 }
1087 }
1088}
1089
1090#[cfg(test)]
1091pub mod tests {
1092 use hex_literal::hex;
1093 use hopr_crypto_random::Randomizable;
1094 use hopr_crypto_types::{
1095 keypairs::{ChainKeypair, Keypair},
1096 types::{Challenge, CurvePoint, HalfKey, Hash, Response},
1097 };
1098 use hopr_primitive_types::{
1099 prelude::UnitaryFloatOps,
1100 primitives::{Address, EthereumChallenge, U256},
1101 };
1102
1103 use super::*;
1104
1105 lazy_static::lazy_static! {
1106 static ref ALICE: ChainKeypair = ChainKeypair::from_secret(&hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775")).expect("lazy static keypair should be constructible");
1107 static ref BOB: ChainKeypair = ChainKeypair::from_secret(&hex!("48680484c6fc31bc881a0083e6e32b6dc789f9eaba0f8b981429fd346c697f8c")).expect("lazy static keypair should be constructible");
1108 }
1109
1110 #[cfg(feature = "serde")]
1111 const BINCODE_CONFIGURATION: bincode::config::Configuration = bincode::config::standard()
1112 .with_little_endian()
1113 .with_variable_int_encoding();
1114
1115 #[test]
1116 pub fn test_win_prob_to_f64() -> anyhow::Result<()> {
1117 assert_eq!(0.0f64, WinningProbability::NEVER.as_f64());
1118
1119 assert_eq!(1.0f64, WinningProbability::ALWAYS.as_f64());
1120
1121 let mut test_bit_string = [0xffu8; 7];
1122 test_bit_string[0] = 0x7f;
1123 assert_eq!(0.5f64, WinningProbability::from(&test_bit_string).as_f64());
1124
1125 test_bit_string[0] = 0x3f;
1126 assert_eq!(0.25f64, WinningProbability::from(&test_bit_string).as_f64());
1127
1128 test_bit_string[0] = 0x1f;
1129 assert_eq!(0.125f64, WinningProbability::from(&test_bit_string).as_f64());
1130
1131 Ok(())
1132 }
1133
1134 #[test]
1135 pub fn test_f64_to_win_prob() -> anyhow::Result<()> {
1136 assert_eq!([0u8; 7], WinningProbability::try_from(0.0f64)?.as_encoded());
1137
1138 let mut test_bit_string = [0xffu8; 7];
1139 assert_eq!(test_bit_string, WinningProbability::try_from(1.0f64)?.as_encoded());
1140
1141 test_bit_string[0] = 0x7f;
1142 assert_eq!(test_bit_string, WinningProbability::try_from(0.5f64)?.as_encoded());
1143
1144 test_bit_string[0] = 0x3f;
1145 assert_eq!(test_bit_string, WinningProbability::try_from(0.25f64)?.as_encoded());
1146
1147 test_bit_string[0] = 0x1f;
1148 assert_eq!(test_bit_string, WinningProbability::try_from(0.125f64)?.as_encoded());
1149
1150 Ok(())
1151 }
1152
1153 #[test]
1154 pub fn test_win_prob_approx_eq() -> anyhow::Result<()> {
1155 let wp_0 = WinningProbability(hex!("0020C49BBFFFFF"));
1156 let wp_1 = WinningProbability(hex!("0020C49BA5E34F"));
1157
1158 assert_ne!(wp_0.as_ref(), wp_1.as_ref());
1159 assert_eq!(wp_0, wp_1.as_f64());
1160
1161 Ok(())
1162 }
1163
1164 #[test]
1165 pub fn test_win_prob_back_and_forth() -> anyhow::Result<()> {
1166 for float in [0.1f64, 0.002f64, 0.00001f64, 0.7311111f64, 1.0f64, 0.0f64] {
1167 assert!((float - WinningProbability::try_from_f64(float)?.as_f64()).abs() < f64::EPSILON);
1168 }
1169
1170 Ok(())
1171 }
1172
1173 #[test]
1174 pub fn test_win_prob_must_be_correctly_ordered() {
1175 let increment = WinningProbability::EPSILON * 100.0; let mut prev = WinningProbability::NEVER;
1177 while let Ok(next) = WinningProbability::try_from_f64(prev.as_f64() + increment) {
1178 assert!(prev.approx_cmp(&next).is_lt());
1179 prev = next;
1180 }
1181 }
1182
1183 #[test]
1184 pub fn test_win_prob_epsilon_must_be_never() -> anyhow::Result<()> {
1185 assert!(WinningProbability::NEVER.approx_eq(&WinningProbability::try_from_f64(WinningProbability::EPSILON)?));
1186 Ok(())
1187 }
1188
1189 #[test]
1190 pub fn test_win_prob_bounds_must_be_eq() -> anyhow::Result<()> {
1191 let bound = 0.1 + WinningProbability::EPSILON;
1192 let other = 0.1;
1193 assert!(WinningProbability::try_from_f64(bound)?.approx_eq(&WinningProbability::try_from_f64(other)?));
1194 Ok(())
1195 }
1196
1197 #[test]
1198 pub fn test_ticket_builder_zero_hop() -> anyhow::Result<()> {
1199 let ticket = TicketBuilder::zero_hop()
1200 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1201 .challenge(Default::default())
1202 .build()?;
1203 assert_eq!(0, ticket.index);
1204 assert_eq!(0.0, ticket.win_prob().as_f64());
1205 assert_eq!(0, ticket.channel_epoch);
1206 assert_eq!(
1207 generate_channel_id(&ALICE.public().to_address(), &BOB.public().to_address()),
1208 ticket.channel_id
1209 );
1210 Ok(())
1211 }
1212
1213 #[test]
1214 pub fn test_ticket_serialize_deserialize() -> anyhow::Result<()> {
1215 let initial_ticket = TicketBuilder::default()
1216 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1217 .balance(1.into())
1218 .index(0)
1219 .index_offset(1)
1220 .win_prob(1.0.try_into()?)
1221 .channel_epoch(1)
1222 .challenge(Default::default())
1223 .build_signed(&ALICE, &Default::default())?;
1224
1225 assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
1226
1227 let ticket_bytes: [u8; Ticket::SIZE] = initial_ticket.verified_ticket().clone().into();
1228 assert_eq!(
1229 initial_ticket.verified_ticket(),
1230 &Ticket::try_from(ticket_bytes.as_ref())?
1231 );
1232 Ok(())
1233 }
1234
1235 #[test]
1236 #[cfg(feature = "serde")]
1237 pub fn test_ticket_serialize_deserialize_serde() -> anyhow::Result<()> {
1238 let initial_ticket = TicketBuilder::default()
1239 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1240 .balance(1.into())
1241 .index(0)
1242 .index_offset(1)
1243 .win_prob(1.0.try_into()?)
1244 .channel_epoch(1)
1245 .challenge(Default::default())
1246 .build_signed(&ALICE, &Default::default())?;
1247
1248 assert_eq!(
1249 initial_ticket,
1250 bincode::serde::decode_from_slice(
1251 &bincode::serde::encode_to_vec(&initial_ticket, BINCODE_CONFIGURATION)?,
1252 BINCODE_CONFIGURATION
1253 )
1254 .map(|v| v.0)?
1255 );
1256 Ok(())
1257 }
1258
1259 #[test]
1260 pub fn test_ticket_sign_verify() -> anyhow::Result<()> {
1261 let initial_ticket = TicketBuilder::default()
1262 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1263 .balance(1.into())
1264 .index(0)
1265 .index_offset(1)
1266 .win_prob(1.0.try_into()?)
1267 .channel_epoch(1)
1268 .challenge(Default::default())
1269 .build_signed(&ALICE, &Default::default())?;
1270
1271 assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
1272
1273 let ticket = initial_ticket.leak();
1274 assert!(ticket.verify(&ALICE.public().to_address(), &Default::default()).is_ok());
1275 Ok(())
1276 }
1277
1278 #[test]
1279 pub fn test_zero_hop() -> anyhow::Result<()> {
1280 let ticket = TicketBuilder::zero_hop()
1281 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1282 .challenge(Default::default())
1283 .build_signed(&ALICE, &Default::default())?;
1284
1285 assert!(
1286 ticket
1287 .leak()
1288 .verify(&ALICE.public().to_address(), &Hash::default())
1289 .is_ok()
1290 );
1291 Ok(())
1292 }
1293
1294 fn mock_ticket(
1295 pk: &ChainKeypair,
1296 counterparty: &Address,
1297 domain_separator: Option<Hash>,
1298 challenge: Option<EthereumChallenge>,
1299 ) -> anyhow::Result<VerifiedTicket> {
1300 let win_prob = 1.0f64; let price_per_packet: U256 = 10000000000000000u128.into(); let path_pos = 5u64;
1303
1304 Ok(TicketBuilder::default()
1305 .direction(&pk.public().to_address(), counterparty)
1306 .amount(price_per_packet.div_f64(win_prob)? * U256::from(path_pos))
1307 .index(0)
1308 .index_offset(1)
1309 .win_prob(1.0.try_into()?)
1310 .channel_epoch(4)
1311 .challenge(challenge.unwrap_or_default())
1312 .build_signed(pk, &domain_separator.unwrap_or_default())?)
1313 }
1314
1315 #[test]
1316 fn test_unacknowledged_ticket_challenge_response() -> anyhow::Result<()> {
1317 let hk1 = HalfKey::try_from(hex!("3477d7de923ba3a7d5d72a7d6c43fd78395453532d03b2a1e2b9a7cc9b61bafa").as_ref())?;
1318
1319 let hk2 = HalfKey::try_from(hex!("4471496ef88d9a7d86a92b7676f3c8871a60792a37fae6fc3abc347c3aa3b16b").as_ref())?;
1320
1321 let cp1: CurvePoint = hk1.to_challenge().try_into()?;
1322 let cp2: CurvePoint = hk2.to_challenge().try_into()?;
1323 let cp_sum = CurvePoint::combine(&[&cp1, &cp2]);
1324
1325 let dst = Hash::default();
1326 let ack = mock_ticket(
1327 &ALICE,
1328 &BOB.public().to_address(),
1329 Some(dst),
1330 Some(Challenge::from(cp_sum).to_ethereum_challenge()),
1331 )?
1332 .into_unacknowledged(hk1)
1333 .acknowledge(&hk2)?;
1334
1335 assert!(ack.is_winning(&BOB, &dst), "ticket must be winning");
1336 Ok(())
1337 }
1338
1339 #[test]
1340 #[cfg(feature = "serde")]
1341 fn test_acknowledged_ticket_serde() -> anyhow::Result<()> {
1342 let response =
1343 Response::try_from(hex!("876a41ee5fb2d27ac14d8e8d552692149627c2f52330ba066f9e549aef762f73").as_ref())?;
1344
1345 let dst = Hash::default();
1346
1347 let ticket = mock_ticket(
1348 &ALICE,
1349 &BOB.public().to_address(),
1350 Some(dst),
1351 Some(response.to_challenge().into()),
1352 )?;
1353
1354 let acked_ticket = ticket.into_acknowledged(response);
1355
1356 let mut deserialized_ticket = bincode::serde::decode_from_slice(
1357 &bincode::serde::encode_to_vec(&acked_ticket, BINCODE_CONFIGURATION)?,
1358 BINCODE_CONFIGURATION,
1359 )
1360 .map(|v| v.0)?;
1361 assert_eq!(acked_ticket, deserialized_ticket);
1362
1363 assert!(deserialized_ticket.is_winning(&BOB, &dst));
1364
1365 deserialized_ticket.status = super::AcknowledgedTicketStatus::BeingAggregated;
1366
1367 assert_eq!(
1368 deserialized_ticket,
1369 bincode::serde::decode_from_slice(
1370 &bincode::serde::encode_to_vec(&deserialized_ticket, BINCODE_CONFIGURATION)?,
1371 BINCODE_CONFIGURATION,
1372 )
1373 .map(|v| v.0)?
1374 );
1375 Ok(())
1376 }
1377
1378 #[test]
1379 fn test_ticket_entire_ticket_transfer_flow() -> anyhow::Result<()> {
1380 let hk1 = HalfKey::random();
1381 let hk2 = HalfKey::random();
1382 let resp = Response::from_half_keys(&hk1, &hk2)?;
1383
1384 let verified = TicketBuilder::default()
1385 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1386 .balance(1.into())
1387 .index(0)
1388 .index_offset(1)
1389 .win_prob(1.0.try_into()?)
1390 .channel_epoch(1)
1391 .challenge(resp.to_challenge().to_ethereum_challenge())
1392 .build_signed(&ALICE, &Default::default())?;
1393
1394 let unack = verified.into_unacknowledged(hk1);
1395 let acknowledged = unack.acknowledge(&hk2).expect("should acknowledge");
1396
1397 let redeemable_1 = acknowledged.clone().into_redeemable(&BOB, &Hash::default())?;
1398
1399 let transferable = acknowledged.into_transferable(&BOB, &Hash::default())?;
1400
1401 let redeemable_2 = transferable.into_redeemable(&ALICE.public().to_address(), &Hash::default())?;
1402
1403 assert_eq!(redeemable_1, redeemable_2);
1404 assert_eq!(redeemable_1.vrf_params.V, redeemable_2.vrf_params.V);
1405 Ok(())
1406 }
1407}