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, instrument};
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.get_v_encoded_point().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]
374 pub fn challenge(mut self, challenge: Challenge) -> Self {
375 self.challenge = Some(challenge.to_ethereum_challenge());
376 self
377 }
378
379 pub fn eth_challenge(mut self, challenge: EthereumChallenge) -> Self {
382 self.challenge = Some(challenge);
383 self
384 }
385
386 #[must_use]
389 pub fn signature(mut self, signature: Signature) -> Self {
390 self.signature = Some(signature);
391 self
392 }
393
394 pub fn build(self) -> errors::Result<Ticket> {
398 let amount = match (self.amount, self.balance) {
399 (Some(amount), None) if amount.lt(&10_u128.pow(25).into()) => HoprBalance::from(amount),
400 (None, Some(balance)) if balance.amount().lt(&10_u128.pow(25).into()) => balance,
401 (None, None) => return Err(InvalidInputData("missing ticket amount".into())),
402 (Some(_), Some(_)) => {
403 return Err(InvalidInputData(
404 "either amount or balance must be set but not both".into(),
405 ));
406 }
407 _ => {
408 return Err(InvalidInputData(
409 "tickets may not have more than 1% of total supply".into(),
410 ));
411 }
412 };
413
414 if self.index > (1_u64 << 48) {
415 return Err(InvalidInputData("cannot hold ticket indices larger than 2^48".into()));
416 }
417
418 if self.channel_epoch > (1_u32 << 24) {
419 return Err(InvalidInputData("cannot hold channel epoch larger than 2^24".into()));
420 }
421
422 if self.index_offset < 1 {
423 return Err(InvalidInputData(
424 "ticket index offset must be greater or equal to 1".into(),
425 ));
426 }
427
428 Ok(Ticket {
429 channel_id: self.channel_id.ok_or(InvalidInputData("missing channel id".into()))?,
430 amount,
431 index: self.index,
432 index_offset: self.index_offset,
433 encoded_win_prob: self.win_prob.into(),
434 channel_epoch: self.channel_epoch,
435 challenge: self
436 .challenge
437 .ok_or(InvalidInputData("missing ticket challenge".into()))?,
438 signature: self.signature,
439 })
440 }
441
442 pub fn build_signed(self, signer: &ChainKeypair, domain_separator: &Hash) -> errors::Result<VerifiedTicket> {
445 if self.signature.is_none() {
446 Ok(self.build()?.sign(signer, domain_separator))
447 } else {
448 Err(InvalidInputData("signature already set".into()))
449 }
450 }
451
452 pub fn build_verified(self, hash: Hash) -> errors::Result<VerifiedTicket> {
457 if let Some(signature) = self.signature {
458 let issuer = signature.recover_from_hash(&hash)?.to_address();
459 Ok(VerifiedTicket(self.build()?, hash, issuer))
460 } else {
461 Err(InvalidInputData("signature is missing".into()))
462 }
463 }
464}
465
466impl From<&Ticket> for TicketBuilder {
467 fn from(value: &Ticket) -> Self {
468 Self {
469 channel_id: Some(value.channel_id),
470 amount: None,
471 balance: Some(value.amount),
472 index: value.index,
473 index_offset: value.index_offset,
474 channel_epoch: value.channel_epoch,
475 win_prob: value.encoded_win_prob.into(),
476 challenge: Some(value.challenge),
477 signature: None,
478 }
479 }
480}
481
482impl From<Ticket> for TicketBuilder {
483 fn from(value: Ticket) -> Self {
484 Self::from(&value)
485 }
486}
487
488#[cfg_attr(doc, aquamarine::aquamarine)]
489#[derive(Clone, Debug, PartialEq, Eq)]
510#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
511pub struct Ticket {
512 pub channel_id: Hash,
515 pub amount: HoprBalance, pub index: u64, pub index_offset: u32, pub encoded_win_prob: EncodedWinProb, pub channel_epoch: u32, pub challenge: EthereumChallenge,
532 pub signature: Option<Signature>,
534}
535
536impl PartialOrd for Ticket {
537 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
538 Some(self.cmp(other))
539 }
540}
541
542impl Ord for Ticket {
543 fn cmp(&self, other: &Self) -> Ordering {
544 match self.channel_id.cmp(&other.channel_id) {
547 Ordering::Equal => match self.channel_epoch.cmp(&other.channel_epoch) {
548 Ordering::Equal => self.index.cmp(&other.index),
549 Ordering::Greater => Ordering::Greater,
550 Ordering::Less => Ordering::Less,
551 },
552 Ordering::Greater => Ordering::Greater,
553 Ordering::Less => Ordering::Less,
554 }
555 }
556}
557
558impl Display for Ticket {
559 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
560 write!(
561 f,
562 "ticket #{}, amount {}, offset {}, epoch {} in channel {}",
563 self.index, self.amount, self.index_offset, self.channel_epoch, self.channel_id
564 )
565 }
566}
567
568impl Ticket {
569 fn encode_without_signature(&self) -> [u8; Self::SIZE - Signature::SIZE] {
570 let mut ret = [0u8; Self::SIZE - Signature::SIZE];
571 let mut offset = 0;
572
573 ret[offset..offset + Hash::SIZE].copy_from_slice(self.channel_id.as_ref());
574 offset += Hash::SIZE;
575
576 ret[offset..offset + 12].copy_from_slice(&self.amount.amount().to_be_bytes()[20..32]);
578 offset += 12;
579
580 ret[offset..offset + 6].copy_from_slice(&self.index.to_be_bytes()[2..8]);
582 offset += 6;
583
584 ret[offset..offset + 4].copy_from_slice(&self.index_offset.to_be_bytes());
585 offset += 4;
586
587 ret[offset..offset + 3].copy_from_slice(&self.channel_epoch.to_be_bytes()[1..4]);
589 offset += 3;
590
591 ret[offset..offset + ENCODED_WIN_PROB_LENGTH].copy_from_slice(&self.encoded_win_prob);
592 offset += ENCODED_WIN_PROB_LENGTH;
593
594 ret[offset..offset + EthereumChallenge::SIZE].copy_from_slice(self.challenge.as_ref());
595
596 ret
597 }
598
599 pub fn get_hash(&self, domain_separator: &Hash) -> Hash {
602 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()]);
604 Hash::create(&[&hex!("1901"), domain_separator.as_ref(), hash_struct.as_ref()])
605 }
606
607 pub fn sign(mut self, signing_key: &ChainKeypair, domain_separator: &Hash) -> VerifiedTicket {
610 let ticket_hash = self.get_hash(domain_separator);
611 self.signature = Some(Signature::sign_hash(&ticket_hash, signing_key));
612 VerifiedTicket(self, ticket_hash, signing_key.public().to_address())
613 }
614
615 #[instrument(level = "trace", skip_all, err)]
623 pub fn verify(self, issuer: &Address, domain_separator: &Hash) -> Result<VerifiedTicket, Box<Ticket>> {
624 let ticket_hash = self.get_hash(domain_separator);
625
626 if let Some(signature) = &self.signature {
627 match signature.recover_from_hash(&ticket_hash) {
628 Ok(pk) if pk.to_address().eq(issuer) => Ok(VerifiedTicket(self, ticket_hash, *issuer)),
629 Err(e) => {
630 error!("failed to verify ticket signature: {e}");
631 Err(self.into())
632 }
633 _ => Err(self.into()),
634 }
635 } else {
636 Err(self.into())
637 }
638 }
639
640 #[inline]
642 pub fn is_aggregated(&self) -> bool {
643 self.index_offset > 1
645 }
646
647 #[inline]
649 pub fn win_prob(&self) -> WinningProbability {
650 WinningProbability(self.encoded_win_prob)
651 }
652}
653
654impl From<Ticket> for [u8; TICKET_SIZE] {
655 fn from(value: Ticket) -> Self {
656 let mut ret = [0u8; TICKET_SIZE];
657 ret[0..Ticket::SIZE - Signature::SIZE].copy_from_slice(value.encode_without_signature().as_ref());
658 ret[Ticket::SIZE - Signature::SIZE..].copy_from_slice(
659 value
660 .signature
661 .expect("cannot serialize ticket without signature")
662 .as_ref(),
663 );
664 ret
665 }
666}
667
668impl TryFrom<&[u8]> for Ticket {
669 type Error = GeneralError;
670
671 fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
672 if value.len() == Self::SIZE {
673 let mut offset = 0;
674
675 let channel_id = Hash::try_from(&value[offset..offset + Hash::SIZE])?;
677 offset += Hash::SIZE;
678
679 let mut amount = [0u8; 32];
680 amount[20..32].copy_from_slice(&value[offset..offset + 12]);
681 offset += 12;
682
683 let mut index = [0u8; 8];
684 index[2..8].copy_from_slice(&value[offset..offset + 6]);
685 offset += 6;
686
687 let mut index_offset = [0u8; 4];
688 index_offset.copy_from_slice(&value[offset..offset + 4]);
689 offset += 4;
690
691 let mut channel_epoch = [0u8; 4];
692 channel_epoch[1..4].copy_from_slice(&value[offset..offset + 3]);
693 offset += 3;
694
695 let win_prob = WinningProbability::try_from(&value[offset..offset + WinningProbability::SIZE])?;
696 offset += WinningProbability::SIZE;
697
698 debug_assert_eq!(offset, ENCODED_TICKET_LENGTH);
699
700 let challenge = EthereumChallenge::try_from(&value[offset..offset + EthereumChallenge::SIZE])?;
701 offset += EthereumChallenge::SIZE;
702
703 let signature = Signature::try_from(&value[offset..offset + Signature::SIZE])?;
704
705 TicketBuilder::default()
707 .channel_id(channel_id)
708 .amount(U256::from_big_endian(&amount))
709 .index(u64::from_be_bytes(index))
710 .index_offset(u32::from_be_bytes(index_offset))
711 .channel_epoch(u32::from_be_bytes(channel_epoch))
712 .win_prob(win_prob)
713 .eth_challenge(challenge)
714 .signature(signature)
715 .build()
716 .map_err(|e| GeneralError::ParseError(format!("ticket build failed: {e}")))
717 } else {
718 Err(GeneralError::ParseError("Ticket".into()))
719 }
720 }
721}
722
723const TICKET_SIZE: usize = ENCODED_TICKET_LENGTH + EthereumChallenge::SIZE + Signature::SIZE;
724
725impl BytesEncodable<TICKET_SIZE> for Ticket {}
726
727#[derive(Debug, Clone, PartialEq, Eq)]
731#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
732pub struct VerifiedTicket(Ticket, Hash, Address);
733
734impl VerifiedTicket {
735 #[inline]
737 pub fn win_prob(&self) -> WinningProbability {
738 self.0.win_prob()
739 }
740
741 pub fn is_winning(&self, response: &Response, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
759 if let Ok(vrf_params) = derive_vrf_parameters(self.1, chain_keypair, domain_separator.as_ref()) {
760 check_ticket_win(
761 &self.1,
762 self.0
763 .signature
764 .as_ref()
765 .expect("verified ticket have always a signature"),
766 &self.0.win_prob(),
767 response,
768 &vrf_params,
769 )
770 } else {
771 error!("cannot derive vrf parameters for {self}");
772 false
773 }
774 }
775
776 #[inline]
778 pub fn verified_ticket(&self) -> &Ticket {
779 &self.0
780 }
781
782 #[inline]
785 pub fn verified_hash(&self) -> &Hash {
786 &self.1
787 }
788
789 #[inline]
793 pub fn verified_issuer(&self) -> &Address {
794 &self.2
795 }
796
797 pub fn verified_signature(&self) -> &Signature {
799 self.0
800 .signature
801 .as_ref()
802 .expect("verified ticket always has a signature")
803 }
804
805 #[inline]
807 pub fn leak(self) -> Ticket {
808 self.0
809 }
810
811 pub fn into_unacknowledged(self, own_key: HalfKey) -> UnacknowledgedTicket {
814 UnacknowledgedTicket { ticket: self, own_key }
815 }
816
817 pub fn into_acknowledged(self, response: Response) -> AcknowledgedTicket {
820 AcknowledgedTicket {
821 status: AcknowledgedTicketStatus::Untouched,
822 ticket: self,
823 response,
824 }
825 }
826}
827
828impl Display for VerifiedTicket {
829 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
830 write!(f, "verified {}", self.0)
831 }
832}
833
834impl PartialOrd for VerifiedTicket {
835 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
836 Some(self.cmp(other))
837 }
838}
839
840impl Ord for VerifiedTicket {
841 fn cmp(&self, other: &Self) -> Ordering {
842 self.0.cmp(&other.0)
843 }
844}
845
846#[derive(Clone, Debug, PartialEq, Eq)]
850#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
851pub struct UnacknowledgedTicket {
852 pub ticket: VerifiedTicket,
853 pub(crate) own_key: HalfKey,
854}
855
856impl UnacknowledgedTicket {
857 #[inline]
859 pub fn verified_ticket(&self) -> &Ticket {
860 self.ticket.verified_ticket()
861 }
862
863 pub fn acknowledge(self, acknowledgement: &HalfKey) -> crate::errors::Result<AcknowledgedTicket> {
867 let response = Response::from_half_keys(&self.own_key, acknowledgement)?;
868 debug!(ticket = %self.ticket, response = response.to_hex(), "acknowledging ticket using response");
869
870 if self.ticket.verified_ticket().challenge == response.to_challenge()?.to_ethereum_challenge() {
871 Ok(self.ticket.into_acknowledged(response))
872 } else {
873 Err(CryptoError::InvalidChallenge.into())
874 }
875 }
876}
877
878#[repr(u8)]
880#[derive(
881 Clone,
882 Copy,
883 Debug,
884 Default,
885 Eq,
886 PartialEq,
887 strum::Display,
888 strum::EnumString,
889 num_enum::IntoPrimitive,
890 num_enum::TryFromPrimitive,
891)]
892#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
893#[strum(serialize_all = "PascalCase")]
894pub enum AcknowledgedTicketStatus {
895 #[default]
897 Untouched = 0,
898 BeingRedeemed = 1,
900 BeingAggregated = 2,
902}
903
904#[derive(Clone, Debug, PartialEq, Eq)]
906#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
907pub struct AcknowledgedTicket {
908 #[cfg_attr(feature = "serde", serde(default))]
909 pub status: AcknowledgedTicketStatus,
910 pub ticket: VerifiedTicket,
911 pub response: Response,
912}
913
914impl PartialOrd for AcknowledgedTicket {
915 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
916 Some(self.cmp(other))
917 }
918}
919
920impl Ord for AcknowledgedTicket {
921 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
922 self.ticket.cmp(&other.ticket)
923 }
924}
925
926impl AcknowledgedTicket {
927 #[inline]
929 pub fn verified_ticket(&self) -> &Ticket {
930 self.ticket.verified_ticket()
931 }
932
933 pub fn is_winning(&self, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
935 self.ticket.is_winning(&self.response, chain_keypair, domain_separator)
936 }
937
938 pub fn into_redeemable(
942 self,
943 chain_keypair: &ChainKeypair,
944 domain_separator: &Hash,
945 ) -> crate::errors::Result<RedeemableTicket> {
946 if chain_keypair.public().to_address().eq(self.ticket.verified_issuer()) {
948 return Err(errors::CoreTypesError::LoopbackTicket);
949 }
950
951 let vrf_params = derive_vrf_parameters(self.ticket.verified_hash(), chain_keypair, domain_separator.as_ref())?;
952
953 Ok(RedeemableTicket {
954 ticket: self.ticket,
955 response: self.response,
956 vrf_params,
957 channel_dst: *domain_separator,
958 })
959 }
960
961 pub fn into_transferable(
964 self,
965 chain_keypair: &ChainKeypair,
966 domain_separator: &Hash,
967 ) -> errors::Result<TransferableWinningTicket> {
968 self.into_redeemable(chain_keypair, domain_separator)
969 .map(TransferableWinningTicket::from)
970 }
971}
972
973impl Display for AcknowledgedTicket {
974 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
975 write!(f, "acknowledged {} in state '{}'", self.ticket, self.status)
976 }
977}
978
979#[derive(Clone, Debug)]
981#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
982pub struct RedeemableTicket {
983 pub ticket: VerifiedTicket,
985 pub response: Response,
987 pub vrf_params: VrfParameters,
989 pub channel_dst: Hash,
991}
992
993impl RedeemableTicket {
994 #[inline]
996 pub fn verified_ticket(&self) -> &Ticket {
997 self.ticket.verified_ticket()
998 }
999}
1000
1001impl PartialEq for RedeemableTicket {
1002 fn eq(&self, other: &Self) -> bool {
1003 self.ticket == other.ticket && self.channel_dst == other.channel_dst && self.response == other.response
1004 }
1005}
1006
1007impl Display for RedeemableTicket {
1008 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1009 write!(f, "redeemable {}", self.ticket)
1010 }
1011}
1012
1013impl From<RedeemableTicket> for AcknowledgedTicket {
1014 fn from(value: RedeemableTicket) -> Self {
1015 Self {
1016 status: AcknowledgedTicketStatus::Untouched,
1017 ticket: value.ticket,
1018 response: value.response,
1019 }
1020 }
1021}
1022
1023#[derive(Debug, Clone)]
1031#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1032pub struct TransferableWinningTicket {
1033 pub ticket: Ticket,
1034 pub response: Response,
1035 pub vrf_params: VrfParameters,
1036 pub signer: Address,
1037}
1038
1039impl TransferableWinningTicket {
1040 pub fn into_redeemable(
1047 self,
1048 expected_issuer: &Address,
1049 domain_separator: &Hash,
1050 ) -> errors::Result<RedeemableTicket> {
1051 if !self.signer.eq(expected_issuer) {
1052 return Err(crate::errors::CoreTypesError::InvalidInputData(
1053 "invalid ticket issuer".into(),
1054 ));
1055 }
1056
1057 let verified_ticket = self
1058 .ticket
1059 .verify(&self.signer, domain_separator)
1060 .map_err(|_| CoreTypesError::CryptoError(CryptoError::SignatureVerification))?;
1061
1062 if check_ticket_win(
1063 verified_ticket.verified_hash(),
1064 verified_ticket.verified_signature(),
1065 &verified_ticket.verified_ticket().win_prob(),
1066 &self.response,
1067 &self.vrf_params,
1068 ) {
1069 Ok(RedeemableTicket {
1070 ticket: verified_ticket,
1071 response: self.response,
1072 vrf_params: self.vrf_params,
1073 channel_dst: *domain_separator,
1074 })
1075 } else {
1076 Err(crate::errors::CoreTypesError::InvalidInputData(
1077 "ticket is not a win".into(),
1078 ))
1079 }
1080 }
1081}
1082
1083impl PartialEq for TransferableWinningTicket {
1084 fn eq(&self, other: &Self) -> bool {
1085 self.ticket == other.ticket && self.signer == other.signer && self.response == other.response
1086 }
1087}
1088
1089impl PartialOrd<Self> for TransferableWinningTicket {
1090 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1091 Some(self.ticket.cmp(&other.ticket))
1092 }
1093}
1094
1095impl From<RedeemableTicket> for TransferableWinningTicket {
1096 fn from(value: RedeemableTicket) -> Self {
1097 Self {
1098 response: value.response,
1099 vrf_params: value.vrf_params,
1100 signer: *value.ticket.verified_issuer(),
1101 ticket: value.ticket.leak(),
1102 }
1103 }
1104}
1105
1106#[cfg(test)]
1107pub mod tests {
1108 use hex_literal::hex;
1109 use hopr_crypto_random::Randomizable;
1110 use hopr_crypto_types::{
1111 keypairs::{ChainKeypair, Keypair},
1112 types::{HalfKey, Hash, Response},
1113 };
1114 use hopr_primitive_types::{
1115 prelude::UnitaryFloatOps,
1116 primitives::{Address, EthereumChallenge, U256},
1117 };
1118
1119 use super::*;
1120
1121 lazy_static::lazy_static! {
1122 static ref ALICE: ChainKeypair = ChainKeypair::from_secret(&hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775")).expect("lazy static keypair should be constructible");
1123 static ref BOB: ChainKeypair = ChainKeypair::from_secret(&hex!("48680484c6fc31bc881a0083e6e32b6dc789f9eaba0f8b981429fd346c697f8c")).expect("lazy static keypair should be constructible");
1124 }
1125
1126 #[cfg(feature = "serde")]
1127 const BINCODE_CONFIGURATION: bincode::config::Configuration = bincode::config::standard()
1128 .with_little_endian()
1129 .with_variable_int_encoding();
1130
1131 #[test]
1132 pub fn test_win_prob_to_f64() -> anyhow::Result<()> {
1133 assert_eq!(0.0f64, WinningProbability::NEVER.as_f64());
1134
1135 assert_eq!(1.0f64, WinningProbability::ALWAYS.as_f64());
1136
1137 let mut test_bit_string = [0xffu8; 7];
1138 test_bit_string[0] = 0x7f;
1139 assert_eq!(0.5f64, WinningProbability::from(&test_bit_string).as_f64());
1140
1141 test_bit_string[0] = 0x3f;
1142 assert_eq!(0.25f64, WinningProbability::from(&test_bit_string).as_f64());
1143
1144 test_bit_string[0] = 0x1f;
1145 assert_eq!(0.125f64, WinningProbability::from(&test_bit_string).as_f64());
1146
1147 Ok(())
1148 }
1149
1150 #[test]
1151 pub fn test_f64_to_win_prob() -> anyhow::Result<()> {
1152 assert_eq!([0u8; 7], WinningProbability::try_from(0.0f64)?.as_encoded());
1153
1154 let mut test_bit_string = [0xffu8; 7];
1155 assert_eq!(test_bit_string, WinningProbability::try_from(1.0f64)?.as_encoded());
1156
1157 test_bit_string[0] = 0x7f;
1158 assert_eq!(test_bit_string, WinningProbability::try_from(0.5f64)?.as_encoded());
1159
1160 test_bit_string[0] = 0x3f;
1161 assert_eq!(test_bit_string, WinningProbability::try_from(0.25f64)?.as_encoded());
1162
1163 test_bit_string[0] = 0x1f;
1164 assert_eq!(test_bit_string, WinningProbability::try_from(0.125f64)?.as_encoded());
1165
1166 Ok(())
1167 }
1168
1169 #[test]
1170 pub fn test_win_prob_approx_eq() -> anyhow::Result<()> {
1171 let wp_0 = WinningProbability(hex!("0020C49BBFFFFF"));
1172 let wp_1 = WinningProbability(hex!("0020C49BA5E34F"));
1173
1174 assert_ne!(wp_0.as_ref(), wp_1.as_ref());
1175 assert_eq!(wp_0, wp_1.as_f64());
1176
1177 Ok(())
1178 }
1179
1180 #[test]
1181 pub fn test_win_prob_back_and_forth() -> anyhow::Result<()> {
1182 for float in [0.1f64, 0.002f64, 0.00001f64, 0.7311111f64, 1.0f64, 0.0f64] {
1183 assert!((float - WinningProbability::try_from_f64(float)?.as_f64()).abs() < f64::EPSILON);
1184 }
1185
1186 Ok(())
1187 }
1188
1189 #[test]
1190 pub fn test_win_prob_must_be_correctly_ordered() {
1191 let increment = WinningProbability::EPSILON * 100.0; let mut prev = WinningProbability::NEVER;
1193 while let Ok(next) = WinningProbability::try_from_f64(prev.as_f64() + increment) {
1194 assert!(prev.approx_cmp(&next).is_lt());
1195 prev = next;
1196 }
1197 }
1198
1199 #[test]
1200 pub fn test_win_prob_epsilon_must_be_never() -> anyhow::Result<()> {
1201 assert!(WinningProbability::NEVER.approx_eq(&WinningProbability::try_from_f64(WinningProbability::EPSILON)?));
1202 Ok(())
1203 }
1204
1205 #[test]
1206 pub fn test_win_prob_bounds_must_be_eq() -> anyhow::Result<()> {
1207 let bound = 0.1 + WinningProbability::EPSILON;
1208 let other = 0.1;
1209 assert!(WinningProbability::try_from_f64(bound)?.approx_eq(&WinningProbability::try_from_f64(other)?));
1210 Ok(())
1211 }
1212
1213 #[test]
1214 pub fn test_ticket_builder_zero_hop() -> anyhow::Result<()> {
1215 let ticket = TicketBuilder::zero_hop()
1216 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1217 .eth_challenge(Default::default())
1218 .build()?;
1219 assert_eq!(0, ticket.index);
1220 assert_eq!(0.0, ticket.win_prob().as_f64());
1221 assert_eq!(0, ticket.channel_epoch);
1222 assert_eq!(
1223 generate_channel_id(&ALICE.public().to_address(), &BOB.public().to_address()),
1224 ticket.channel_id
1225 );
1226 Ok(())
1227 }
1228
1229 #[test]
1230 pub fn test_ticket_serialize_deserialize() -> anyhow::Result<()> {
1231 let initial_ticket = TicketBuilder::default()
1232 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1233 .balance(1.into())
1234 .index(0)
1235 .index_offset(1)
1236 .win_prob(1.0.try_into()?)
1237 .channel_epoch(1)
1238 .eth_challenge(Default::default())
1239 .build_signed(&ALICE, &Default::default())?;
1240
1241 assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
1242
1243 let ticket_bytes: [u8; Ticket::SIZE] = initial_ticket.verified_ticket().clone().into();
1244 assert_eq!(
1245 initial_ticket.verified_ticket(),
1246 &Ticket::try_from(ticket_bytes.as_ref())?
1247 );
1248 Ok(())
1249 }
1250
1251 #[test]
1252 #[cfg(feature = "serde")]
1253 pub fn test_ticket_serialize_deserialize_serde() -> anyhow::Result<()> {
1254 let initial_ticket = TicketBuilder::default()
1255 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1256 .balance(1.into())
1257 .index(0)
1258 .index_offset(1)
1259 .win_prob(1.0.try_into()?)
1260 .channel_epoch(1)
1261 .eth_challenge(Default::default())
1262 .build_signed(&ALICE, &Default::default())?;
1263
1264 assert_eq!(
1265 initial_ticket,
1266 bincode::serde::decode_from_slice(
1267 &bincode::serde::encode_to_vec(&initial_ticket, BINCODE_CONFIGURATION)?,
1268 BINCODE_CONFIGURATION
1269 )
1270 .map(|v| v.0)?
1271 );
1272 Ok(())
1273 }
1274
1275 #[test]
1276 pub fn test_ticket_sign_verify() -> anyhow::Result<()> {
1277 let initial_ticket = TicketBuilder::default()
1278 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1279 .balance(1.into())
1280 .index(0)
1281 .index_offset(1)
1282 .win_prob(1.0.try_into()?)
1283 .channel_epoch(1)
1284 .eth_challenge(Default::default())
1285 .build_signed(&ALICE, &Default::default())?;
1286
1287 assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
1288
1289 let ticket = initial_ticket.leak();
1290 assert!(ticket.verify(&ALICE.public().to_address(), &Default::default()).is_ok());
1291 Ok(())
1292 }
1293
1294 #[test]
1295 pub fn test_zero_hop() -> anyhow::Result<()> {
1296 let ticket = TicketBuilder::zero_hop()
1297 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1298 .eth_challenge(Default::default())
1299 .build_signed(&ALICE, &Default::default())?;
1300
1301 assert!(
1302 ticket
1303 .leak()
1304 .verify(&ALICE.public().to_address(), &Hash::default())
1305 .is_ok()
1306 );
1307 Ok(())
1308 }
1309
1310 fn mock_ticket(
1311 pk: &ChainKeypair,
1312 counterparty: &Address,
1313 domain_separator: Option<Hash>,
1314 challenge: Option<EthereumChallenge>,
1315 ) -> anyhow::Result<VerifiedTicket> {
1316 let win_prob = 1.0f64; let price_per_packet: U256 = 10000000000000000u128.into(); let path_pos = 5u64;
1319
1320 Ok(TicketBuilder::default()
1321 .direction(&pk.public().to_address(), counterparty)
1322 .amount(price_per_packet.div_f64(win_prob)? * U256::from(path_pos))
1323 .index(0)
1324 .index_offset(1)
1325 .win_prob(1.0.try_into()?)
1326 .channel_epoch(4)
1327 .eth_challenge(challenge.unwrap_or_default())
1328 .build_signed(pk, &domain_separator.unwrap_or_default())?)
1329 }
1330
1331 #[test]
1332 fn test_unacknowledged_ticket_challenge_response() -> anyhow::Result<()> {
1333 let hk1 = HalfKey::try_from(hex!("3477d7de923ba3a7d5d72a7d6c43fd78395453532d03b2a1e2b9a7cc9b61bafa").as_ref())?;
1334
1335 let hk2 = HalfKey::try_from(hex!("4471496ef88d9a7d86a92b7676f3c8871a60792a37fae6fc3abc347c3aa3b16b").as_ref())?;
1336
1337 let challenge = Response::from_half_keys(&hk1, &hk2)?.to_challenge()?;
1338
1339 let dst = Hash::default();
1340 let ack = mock_ticket(
1341 &ALICE,
1342 &BOB.public().to_address(),
1343 Some(dst),
1344 Some(challenge.to_ethereum_challenge()),
1345 )?
1346 .into_unacknowledged(hk1)
1347 .acknowledge(&hk2)?;
1348
1349 assert!(ack.is_winning(&BOB, &dst), "ticket must be winning");
1350 Ok(())
1351 }
1352
1353 #[test]
1354 #[cfg(feature = "serde")]
1355 fn test_acknowledged_ticket_serde() -> anyhow::Result<()> {
1356 let response =
1357 Response::try_from(hex!("876a41ee5fb2d27ac14d8e8d552692149627c2f52330ba066f9e549aef762f73").as_ref())?;
1358
1359 let dst = Hash::default();
1360
1361 let ticket = mock_ticket(
1362 &ALICE,
1363 &BOB.public().to_address(),
1364 Some(dst),
1365 Some(response.to_challenge()?.to_ethereum_challenge()),
1366 )?;
1367
1368 let acked_ticket = ticket.into_acknowledged(response);
1369
1370 let mut deserialized_ticket = bincode::serde::decode_from_slice(
1371 &bincode::serde::encode_to_vec(&acked_ticket, BINCODE_CONFIGURATION)?,
1372 BINCODE_CONFIGURATION,
1373 )
1374 .map(|v| v.0)?;
1375 assert_eq!(acked_ticket, deserialized_ticket);
1376
1377 assert!(deserialized_ticket.is_winning(&BOB, &dst));
1378
1379 deserialized_ticket.status = super::AcknowledgedTicketStatus::BeingAggregated;
1380
1381 assert_eq!(
1382 deserialized_ticket,
1383 bincode::serde::decode_from_slice(
1384 &bincode::serde::encode_to_vec(&deserialized_ticket, BINCODE_CONFIGURATION)?,
1385 BINCODE_CONFIGURATION,
1386 )
1387 .map(|v| v.0)?
1388 );
1389 Ok(())
1390 }
1391
1392 #[test]
1393 fn test_ticket_entire_ticket_transfer_flow() -> anyhow::Result<()> {
1394 let hk1 = HalfKey::random();
1395 let hk2 = HalfKey::random();
1396 let resp = Response::from_half_keys(&hk1, &hk2)?;
1397
1398 let verified = TicketBuilder::default()
1399 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1400 .balance(1.into())
1401 .index(0)
1402 .index_offset(1)
1403 .win_prob(1.0.try_into()?)
1404 .channel_epoch(1)
1405 .challenge(resp.to_challenge()?)
1406 .build_signed(&ALICE, &Default::default())?;
1407
1408 let unack = verified.into_unacknowledged(hk1);
1409 let acknowledged = unack.acknowledge(&hk2).expect("should acknowledge");
1410
1411 let redeemable_1 = acknowledged.clone().into_redeemable(&BOB, &Hash::default())?;
1412
1413 let transferable = acknowledged.into_transferable(&BOB, &Hash::default())?;
1414
1415 let redeemable_2 = transferable.into_redeemable(&ALICE.public().to_address(), &Hash::default())?;
1416
1417 assert_eq!(redeemable_1, redeemable_2);
1418 assert_eq!(redeemable_1.vrf_params.V, redeemable_2.vrf_params.V);
1419 Ok(())
1420 }
1421}