1use std::{
2 cmp::Ordering,
3 fmt::{Display, Formatter},
4 str::FromStr,
5};
6
7use hex_literal::hex;
8use hopr_crypto_types::prelude::*;
9use hopr_primitive_types::prelude::*;
10use tracing::{error, instrument};
11
12use crate::{
13 errors,
14 errors::CoreTypesError,
15 prelude::{ChannelId, CoreTypesError::InvalidInputData, generate_channel_id},
16};
17
18const ENCODED_WIN_PROB_LENGTH: usize = 7;
22
23pub const REDEEM_CALL_SELECTOR: [u8; 4] = [101, 227, 250, 114];
28
29pub type EncodedWinProb = [u8; ENCODED_WIN_PROB_LENGTH];
31
32#[derive(Clone, Copy, Debug, Hash)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct WinningProbability(#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] EncodedWinProb);
41
42impl WinningProbability {
43 pub const ALWAYS: Self = Self([0xff; ENCODED_WIN_PROB_LENGTH]);
45 pub const EPSILON: f64 = 0.00000001;
48 pub const NEVER: Self = Self([0u8; ENCODED_WIN_PROB_LENGTH]);
50
51 pub fn as_luck(&self) -> u64 {
53 let mut tmp = [0u8; 8];
54 tmp[1..].copy_from_slice(&self.0);
55 u64::from_be_bytes(tmp)
56 }
57
58 pub fn as_encoded(&self) -> EncodedWinProb {
60 self.0
61 }
62
63 pub fn as_f64(&self) -> f64 {
65 if self.0.eq(&Self::NEVER.0) {
66 return 0.0;
67 }
68
69 if self.0.eq(&Self::ALWAYS.0) {
70 return 1.0;
71 }
72
73 let mut tmp = [0u8; 8];
74 tmp[1..].copy_from_slice(&self.0);
75
76 let tmp = u64::from_be_bytes(tmp);
77
78 let significand: u64 = tmp + 1;
80
81 f64::from_bits((1023u64 << 52) | (significand >> 4)) - 1.0
82 }
83
84 pub fn try_from_f64(win_prob: f64) -> errors::Result<Self> {
86 if !(0.0..=1.0).contains(&win_prob) {
88 return Err(InvalidInputData("winning probability must be in [0.0, 1.0]".into()));
89 }
90
91 if f64_approx_eq(0.0, win_prob, Self::EPSILON) {
92 return Ok(Self::NEVER);
93 }
94
95 if f64_approx_eq(1.0, win_prob, Self::EPSILON) {
96 return Ok(Self::ALWAYS);
97 }
98
99 let tmp: u64 = (win_prob + 1.0).to_bits();
100
101 let significand: u64 = tmp & 0x000fffffffffffffu64;
103
104 let encoded = ((significand - 1) << 4) | 0x000000000000000fu64;
106
107 let mut res = [0u8; 7];
108 res.copy_from_slice(&encoded.to_be_bytes()[1..]);
109
110 Ok(Self(res))
111 }
112
113 pub fn approx_cmp(&self, other: &Self) -> Ordering {
115 let a = self.as_f64();
116 let b = other.as_f64();
117 if !f64_approx_eq(a, b, Self::EPSILON) {
118 a.partial_cmp(&b).expect("finite non-NaN f64 comparison cannot fail")
119 } else {
120 Ordering::Equal
121 }
122 }
123
124 pub fn approx_eq(&self, other: &Self) -> bool {
126 self.approx_cmp(other).is_eq()
127 }
128
129 pub fn lex_cmp(&self, other: &Self) -> Ordering {
131 self.as_luck().cmp(&other.as_luck())
132 }
133
134 pub fn lex_eq(&self, other: &Self) -> bool {
136 self.lex_cmp(other).is_eq()
137 }
138
139 pub fn min(&self, other: &Self) -> Self {
141 if self.approx_cmp(other) == Ordering::Less {
142 *self
143 } else {
144 *other
145 }
146 }
147
148 pub fn max(&self, other: &Self) -> Self {
150 if self.approx_cmp(other) == Ordering::Greater {
151 *self
152 } else {
153 *other
154 }
155 }
156}
157
158impl Default for WinningProbability {
159 fn default() -> Self {
160 Self::ALWAYS
161 }
162}
163
164impl Display for WinningProbability {
165 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
166 write!(f, "{:.8}", self.as_f64())
167 }
168}
169
170impl FromStr for WinningProbability {
171 type Err = CoreTypesError;
172
173 fn from_str(s: &str) -> Result<Self, Self::Err> {
174 f64::from_str(s)
175 .map_err(|e| CoreTypesError::ParseError(format!("failed to parse winning probability: {e}")))
176 .and_then(|v| v.try_into())
177 }
178}
179
180impl From<EncodedWinProb> for WinningProbability {
181 fn from(value: EncodedWinProb) -> Self {
182 Self(value)
183 }
184}
185
186impl<'a> From<&'a EncodedWinProb> for WinningProbability {
187 fn from(value: &'a EncodedWinProb) -> Self {
188 Self(*value)
189 }
190}
191
192impl From<WinningProbability> for EncodedWinProb {
193 fn from(value: WinningProbability) -> Self {
194 value.0
195 }
196}
197
198impl From<u64> for WinningProbability {
199 fn from(value: u64) -> Self {
200 let mut ret = Self::default();
201 ret.0.copy_from_slice(&value.to_be_bytes()[1..]);
202 ret
203 }
204}
205
206impl TryFrom<f64> for WinningProbability {
207 type Error = CoreTypesError;
208
209 fn try_from(value: f64) -> Result<Self, Self::Error> {
210 Self::try_from_f64(value)
211 }
212}
213
214impl From<WinningProbability> for f64 {
215 fn from(value: WinningProbability) -> Self {
216 value.as_f64()
217 }
218}
219
220impl PartialEq<f64> for WinningProbability {
221 fn eq(&self, other: &f64) -> bool {
222 f64_approx_eq(self.as_f64(), *other, Self::EPSILON)
223 }
224}
225
226impl PartialEq<WinningProbability> for f64 {
227 fn eq(&self, other: &WinningProbability) -> bool {
228 f64_approx_eq(*self, other.as_f64(), WinningProbability::EPSILON)
229 }
230}
231
232impl PartialEq<EncodedWinProb> for WinningProbability {
233 fn eq(&self, other: &EncodedWinProb) -> bool {
234 self.0.eq(other)
235 }
236}
237
238impl AsRef<[u8]> for WinningProbability {
239 fn as_ref(&self) -> &[u8] {
240 &self.0
241 }
242}
243
244impl<'a> TryFrom<&'a [u8]> for WinningProbability {
245 type Error = GeneralError;
246
247 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
248 value
249 .try_into()
250 .map(Self)
251 .map_err(|_| GeneralError::ParseError("WinningProbability".into()))
252 }
253}
254
255impl BytesRepresentable for WinningProbability {
256 const SIZE: usize = ENCODED_WIN_PROB_LENGTH;
257}
258
259pub(crate) fn check_ticket_win(
263 ticket_hash: &Hash,
264 ticket_signature: &Signature,
265 win_prob: &WinningProbability,
266 response: &Response,
267 vrf_params: &VrfParameters,
268) -> bool {
269 let mut computed_ticket_luck = [0u8; 8];
271 computed_ticket_luck[1..].copy_from_slice(
272 &Hash::create(&[
273 ticket_hash.as_ref(),
274 &vrf_params.get_v_encoded_point().as_bytes()[1..], response.as_ref(),
276 ticket_signature.as_ref(),
277 ])
278 .as_ref()[0..7],
279 );
280
281 u64::from_be_bytes(computed_ticket_luck) <= win_prob.as_luck()
282}
283
284#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
286pub struct TicketId {
287 pub id: ChannelId,
288 pub epoch: u32,
289 pub index: u64,
290}
291
292impl Display for TicketId {
293 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
294 write!(f, "ticket #{}, epoch {} in channel {}", self.index, self.epoch, self.id)
295 }
296}
297
298impl From<&VerifiedTicket> for TicketId {
299 fn from(value: &VerifiedTicket) -> Self {
300 Self {
301 id: value.channel_id,
302 epoch: value.ticket.channel_epoch,
303 index: value.ticket.index,
304 }
305 }
306}
307
308#[derive(Debug, Clone, smart_default::SmartDefault)]
315pub struct TicketBuilder {
316 counterparty: Option<Address>,
317 amount: Option<U256>,
318 balance: Option<HoprBalance>,
319 #[default = 0]
320 index: u64,
321 #[default = 1]
322 channel_epoch: u32,
323 win_prob: WinningProbability,
324 challenge: Option<EthereumChallenge>,
325 signature: Option<Signature>,
326}
327
328impl TicketBuilder {
329 #[must_use]
331 pub fn zero_hop() -> Self {
332 Self {
333 index: 0,
334 amount: Some(U256::zero()),
335 win_prob: WinningProbability::NEVER,
336 channel_epoch: 0,
337 ..Default::default()
338 }
339 }
340
341 #[must_use]
343 pub fn counterparty<A: Into<Address>>(mut self, counterparty: A) -> Self {
344 self.counterparty = Some(counterparty.into());
345 self
346 }
347
348 #[must_use]
351 pub fn amount<T: Into<U256>>(mut self, amount: T) -> Self {
352 self.amount = Some(amount.into());
353 self.balance = None;
354 self
355 }
356
357 #[must_use]
360 pub fn balance(mut self, balance: HoprBalance) -> Self {
361 self.balance = Some(balance);
362 self.amount = None;
363 self
364 }
365
366 #[must_use]
370 pub fn index(mut self, index: u64) -> Self {
371 self.index = index;
372 self
373 }
374
375 #[must_use]
379 pub fn channel_epoch(mut self, channel_epoch: u32) -> Self {
380 self.channel_epoch = channel_epoch;
381 self
382 }
383
384 #[must_use]
387 pub fn win_prob(mut self, win_prob: WinningProbability) -> Self {
388 self.win_prob = win_prob;
389 self
390 }
391
392 #[must_use]
396 pub fn challenge(mut self, challenge: Challenge) -> Self {
397 self.challenge = Some(challenge.to_ethereum_challenge());
398 self
399 }
400
401 pub fn eth_challenge(mut self, challenge: EthereumChallenge) -> Self {
404 self.challenge = Some(challenge);
405 self
406 }
407
408 #[must_use]
411 pub fn signature(mut self, signature: Signature) -> Self {
412 self.signature = Some(signature);
413 self
414 }
415
416 pub fn build(self) -> errors::Result<Ticket> {
420 let amount = match (self.amount, self.balance) {
421 (Some(amount), None) if amount.lt(&10_u128.pow(25).into()) => HoprBalance::from(amount),
422 (None, Some(balance)) if balance.amount().lt(&10_u128.pow(25).into()) => balance,
423 (None, None) => return Err(InvalidInputData("missing ticket amount".into())),
424 (Some(_), Some(_)) => {
425 return Err(InvalidInputData(
426 "either amount or balance must be set but not both".into(),
427 ));
428 }
429 _ => {
430 return Err(InvalidInputData(
431 "tickets may not have more than 1% of total supply".into(),
432 ));
433 }
434 };
435
436 if self.index >= (1_u64 << 48) {
437 return Err(InvalidInputData(
438 "cannot hold ticket indices larger than 2^48 - 1".into(),
439 ));
440 }
441
442 if self.channel_epoch >= (1_u32 << 24) {
443 return Err(InvalidInputData(
444 "cannot hold channel epoch larger than 2^24 - 1".into(),
445 ));
446 }
447
448 Ok(Ticket {
449 counterparty: self.counterparty.ok_or(InvalidInputData("missing channel id".into()))?,
450 amount,
451 index: self.index,
452 encoded_win_prob: self.win_prob.into(),
453 channel_epoch: self.channel_epoch,
454 challenge: self
455 .challenge
456 .ok_or(InvalidInputData("missing ticket challenge".into()))?,
457 signature: self.signature,
458 })
459 }
460
461 pub fn build_signed(self, signer: &ChainKeypair, domain_separator: &Hash) -> errors::Result<VerifiedTicket> {
464 if self.signature.is_none() {
465 Ok(self.build()?.sign(signer, domain_separator))
466 } else {
467 Err(InvalidInputData("signature already set".into()))
468 }
469 }
470
471 pub fn build_verified(self, hash: Hash) -> errors::Result<VerifiedTicket> {
477 if let Some(signature) = self.signature {
478 let issuer = signature.recover_from_hash(&hash)?.to_address();
479 let ticket = self.build()?;
480 Ok(VerifiedTicket {
481 hash,
482 issuer,
483 channel_id: generate_channel_id(&issuer, &ticket.counterparty),
484 ticket,
485 })
486 } else {
487 Err(InvalidInputData("signature is missing".into()))
488 }
489 }
490}
491
492impl From<&Ticket> for TicketBuilder {
493 fn from(value: &Ticket) -> Self {
494 Self {
495 counterparty: Some(value.counterparty),
496 amount: None,
497 balance: Some(value.amount),
498 index: value.index,
499 channel_epoch: value.channel_epoch,
500 win_prob: value.encoded_win_prob.into(),
501 challenge: Some(value.challenge),
502 signature: None,
503 }
504 }
505}
506
507impl From<Ticket> for TicketBuilder {
508 fn from(value: Ticket) -> Self {
509 Self::from(&value)
510 }
511}
512
513#[cfg_attr(doc, aquamarine::aquamarine)]
514#[derive(Clone, Copy, Debug, PartialEq, Eq)]
535#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
536pub struct Ticket {
537 pub counterparty: Address,
539 pub amount: HoprBalance, pub index: u64, pub encoded_win_prob: EncodedWinProb, pub channel_epoch: u32, pub challenge: EthereumChallenge,
555 pub signature: Option<Signature>,
557}
558
559impl Display for Ticket {
560 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
561 write!(
562 f,
563 "ticket #{}, amount {}, epoch {} with {}",
564 self.index, self.amount, self.channel_epoch, self.counterparty
565 )
566 }
567}
568
569impl Ticket {
570 fn encode_tail_without_signature(&self) -> [u8; Self::SIZE - Address::SIZE - Signature::SIZE] {
571 let mut ret = [0u8; Self::SIZE - Address::SIZE - Signature::SIZE];
572 let mut offset = 0;
573 ret[offset..offset + 12].copy_from_slice(&self.amount.amount().to_be_bytes()[20..32]);
575 offset += 12;
576
577 ret[offset..offset + 6].copy_from_slice(&self.index.to_be_bytes()[2..8]);
579 offset += 6;
580
581 ret[offset..offset + 3].copy_from_slice(&self.channel_epoch.to_be_bytes()[1..4]);
583 offset += 3;
584
585 ret[offset..offset + ENCODED_WIN_PROB_LENGTH].copy_from_slice(&self.encoded_win_prob);
586 offset += ENCODED_WIN_PROB_LENGTH;
587
588 ret[offset..offset + EthereumChallenge::SIZE].copy_from_slice(self.challenge.as_ref());
589
590 ret
591 }
592
593 fn encode_for_transfer(&self) -> [u8; Self::SIZE - Signature::SIZE] {
594 let mut ret = [0u8; Self::SIZE - Signature::SIZE];
595 let mut offset = 0;
596
597 ret[offset..offset + Address::SIZE].copy_from_slice(self.counterparty.as_ref());
598 offset += Address::SIZE;
599
600 ret[offset..].copy_from_slice(&self.encode_tail_without_signature());
601 ret
602 }
603
604 fn encode_for_signing(&self, issuer: &Address) -> (ChannelId, [u8; ON_CHAIN_TICKET_SIZE - Signature::SIZE]) {
605 let mut ret = [0u8; ON_CHAIN_TICKET_SIZE - Signature::SIZE];
606 let mut offset = 0;
607
608 let channel_id = generate_channel_id(issuer, &self.counterparty);
609 ret[offset..offset + Hash::SIZE].copy_from_slice(channel_id.as_ref());
610 offset += Hash::SIZE;
611
612 ret[offset..].copy_from_slice(&self.encode_tail_without_signature());
613 (channel_id, ret)
614 }
615
616 fn get_hash(&self, issuer: &Address, domain_separator: &Hash) -> (ChannelId, Hash) {
619 let (channel_id, hash_struct) = self.encode_for_signing(issuer);
620 let ticket_hash = Hash::create(&[hash_struct.as_ref()]); let hash_struct = Hash::create(&[&REDEEM_CALL_SELECTOR, &[0u8; 28], ticket_hash.as_ref()]);
622 (
623 channel_id,
624 Hash::create(&[&hex!("1901"), domain_separator.as_ref(), hash_struct.as_ref()]),
625 )
626 }
627
628 pub fn sign(mut self, signing_key: &ChainKeypair, domain_separator: &Hash) -> VerifiedTicket {
631 let (channel_id, ticket_hash) = self.get_hash(signing_key.public().as_ref(), domain_separator);
632 self.signature = Some(Signature::sign_hash(&ticket_hash, signing_key));
633 VerifiedTicket {
634 ticket: self,
635 hash: ticket_hash,
636 issuer: signing_key.public().to_address(),
637 channel_id,
638 }
639 }
640
641 #[instrument(level = "trace", skip_all, err)]
649 pub fn verify(self, issuer: &Address, domain_separator: &Hash) -> Result<VerifiedTicket, Box<Ticket>> {
650 let (channel_id, ticket_hash) = self.get_hash(issuer, domain_separator);
651
652 if let Some(signature) = &self.signature {
653 match signature.recover_from_hash(&ticket_hash) {
654 Ok(pk) if pk.to_address().eq(issuer) => Ok(VerifiedTicket {
655 ticket: self,
656 hash: ticket_hash,
657 issuer: *issuer,
658 channel_id,
659 }),
660 Err(e) => {
661 error!("failed to verify ticket signature: {e}");
662 Err(self.into())
663 }
664 _ => Err(self.into()),
665 }
666 } else {
667 Err(self.into())
668 }
669 }
670
671 #[inline]
673 pub fn win_prob(&self) -> WinningProbability {
674 WinningProbability(self.encoded_win_prob)
675 }
676}
677
678impl From<Ticket> for [u8; TICKET_SIZE] {
679 fn from(value: Ticket) -> Self {
680 let mut ret = [0u8; TICKET_SIZE];
681 ret[0..Ticket::SIZE - Signature::SIZE].copy_from_slice(value.encode_for_transfer().as_ref());
682 ret[Ticket::SIZE - Signature::SIZE..].copy_from_slice(
683 value
684 .signature
685 .expect("cannot serialize ticket without signature")
686 .as_ref(),
687 );
688 ret
689 }
690}
691
692impl TryFrom<&[u8]> for Ticket {
693 type Error = GeneralError;
694
695 fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
696 if value.len() == Self::SIZE {
697 let mut offset = 0;
698
699 let counterparty = Address::try_from(&value[offset..offset + Address::SIZE])?;
700 offset += Address::SIZE;
701
702 let mut amount = [0u8; 32];
703 amount[20..32].copy_from_slice(&value[offset..offset + 12]);
704 offset += 12;
705
706 let mut index = [0u8; 8];
707 index[2..8].copy_from_slice(&value[offset..offset + 6]);
708 offset += 6;
709
710 let mut channel_epoch = [0u8; 4];
711 channel_epoch[1..4].copy_from_slice(&value[offset..offset + 3]);
712 offset += 3;
713
714 let win_prob = WinningProbability::try_from(&value[offset..offset + WinningProbability::SIZE])?;
715 offset += WinningProbability::SIZE;
716
717 let challenge = EthereumChallenge::try_from(&value[offset..offset + EthereumChallenge::SIZE])?;
718 offset += EthereumChallenge::SIZE;
719
720 let signature = Signature::try_from(&value[offset..offset + Signature::SIZE])?;
721
722 TicketBuilder::default()
724 .counterparty(counterparty)
725 .amount(U256::from_big_endian(&amount))
726 .index(u64::from_be_bytes(index))
727 .channel_epoch(u32::from_be_bytes(channel_epoch))
728 .win_prob(win_prob)
729 .eth_challenge(challenge)
730 .signature(signature)
731 .build()
732 .map_err(|e| GeneralError::ParseError(format!("ticket build failed: {e}")))
733 } else {
734 Err(GeneralError::ParseError("Ticket".into()))
735 }
736 }
737}
738
739const TICKET_SIZE: usize = 48 + EthereumChallenge::SIZE + Signature::SIZE;
740
741const ON_CHAIN_TICKET_SIZE: usize = 60 + EthereumChallenge::SIZE + Signature::SIZE;
742
743impl BytesEncodable<TICKET_SIZE> for Ticket {}
744
745#[derive(Debug, Copy, Clone, PartialEq, Eq)]
749#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
750pub struct VerifiedTicket {
751 ticket: Ticket,
752 hash: Hash,
753 issuer: Address,
754 channel_id: ChannelId,
755}
756
757impl VerifiedTicket {
758 #[inline]
760 pub fn win_prob(&self) -> WinningProbability {
761 self.ticket.win_prob()
762 }
763
764 pub fn is_winning(&self, response: &Response, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
782 if let Ok(vrf_params) = derive_vrf_parameters(self.hash, chain_keypair, domain_separator.as_ref()) {
783 check_ticket_win(
784 &self.hash,
785 self.ticket
786 .signature
787 .as_ref()
788 .expect("verified ticket have always a signature"),
789 &self.ticket.win_prob(),
790 response,
791 &vrf_params,
792 )
793 } else {
794 error!("cannot derive vrf parameters for {self}");
795 false
796 }
797 }
798
799 #[inline]
801 pub fn verified_ticket(&self) -> &Ticket {
802 &self.ticket
803 }
804
805 #[inline]
808 pub fn verified_hash(&self) -> &Hash {
809 &self.hash
810 }
811
812 #[inline]
817 pub fn verified_issuer(&self) -> &Address {
818 &self.issuer
819 }
820
821 #[inline]
825 pub fn channel_id(&self) -> &ChannelId {
826 &self.channel_id
827 }
828
829 pub fn verified_signature(&self) -> &Signature {
831 self.ticket
832 .signature
833 .as_ref()
834 .expect("verified ticket always has a signature")
835 }
836
837 #[inline]
839 pub fn leak(self) -> Ticket {
840 self.ticket
841 }
842
843 pub fn into_unacknowledged(self, own_key: HalfKey) -> UnacknowledgedTicket {
846 UnacknowledgedTicket { ticket: self, own_key }
847 }
848
849 pub fn into_acknowledged(self, response: Response) -> AcknowledgedTicket {
851 AcknowledgedTicket {
852 status: AcknowledgedTicketStatus::Untouched,
853 ticket: self,
854 response,
855 }
856 }
857}
858
859impl Display for VerifiedTicket {
860 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
861 write!(f, "verified {} in channel {}", self.ticket, self.channel_id)
862 }
863}
864
865impl PartialOrd for VerifiedTicket {
866 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
867 Some(self.cmp(other))
868 }
869}
870
871impl Ord for VerifiedTicket {
872 fn cmp(&self, other: &Self) -> Ordering {
873 TicketId::from(self).cmp(&TicketId::from(other))
874 }
875}
876
877#[derive(Clone, Copy, Debug, PartialEq, Eq)]
881#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
882pub struct UnacknowledgedTicket {
883 pub ticket: VerifiedTicket,
884 pub(crate) own_key: HalfKey,
885}
886
887impl UnacknowledgedTicket {
888 #[inline]
890 pub fn verified_ticket(&self) -> &Ticket {
891 self.ticket.verified_ticket()
892 }
893
894 pub fn acknowledge(self, acknowledgement: &HalfKey) -> crate::errors::Result<AcknowledgedTicket> {
898 let response = Response::from_half_keys(&self.own_key, acknowledgement)?;
899 tracing::trace!(ticket = %self.ticket, response = response.to_hex(), "acknowledging ticket using response");
900
901 if self.ticket.verified_ticket().challenge == response.to_challenge()?.to_ethereum_challenge() {
902 Ok(self.ticket.into_acknowledged(response))
903 } else {
904 Err(CryptoError::InvalidChallenge.into())
905 }
906 }
907}
908
909impl Display for UnacknowledgedTicket {
910 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
911 write!(f, "unacknowledged {}", self.ticket)
912 }
913}
914
915#[repr(u8)]
917#[derive(
918 Clone,
919 Copy,
920 Debug,
921 Default,
922 Eq,
923 PartialEq,
924 strum::Display,
925 strum::EnumString,
926 num_enum::IntoPrimitive,
927 num_enum::TryFromPrimitive,
928)]
929#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
930#[strum(serialize_all = "PascalCase")]
931pub enum AcknowledgedTicketStatus {
932 #[default]
934 Untouched = 0,
935 BeingRedeemed = 1,
937}
938
939#[derive(Clone, Copy, Debug, PartialEq, Eq)]
941#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
942pub struct AcknowledgedTicket {
943 #[cfg_attr(feature = "serde", serde(default))]
944 pub status: AcknowledgedTicketStatus,
945 pub ticket: VerifiedTicket,
946 pub response: Response,
947}
948
949impl PartialOrd for AcknowledgedTicket {
950 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
951 Some(self.cmp(other))
952 }
953}
954
955impl Ord for AcknowledgedTicket {
956 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
957 self.ticket.cmp(&other.ticket)
958 }
959}
960
961impl AcknowledgedTicket {
962 #[inline]
964 pub fn verified_ticket(&self) -> &Ticket {
965 self.ticket.verified_ticket()
966 }
967
968 pub fn is_winning(&self, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
970 self.ticket.is_winning(&self.response, chain_keypair, domain_separator)
971 }
972
973 pub fn into_redeemable(
979 self,
980 chain_keypair: &ChainKeypair,
981 domain_separator: &Hash,
982 ) -> errors::Result<RedeemableTicket> {
983 if chain_keypair.public().to_address().eq(self.ticket.verified_issuer()) {
985 return Err(errors::CoreTypesError::LoopbackTicket);
986 }
987
988 let vrf_params = derive_vrf_parameters(self.ticket.verified_hash(), chain_keypair, domain_separator.as_ref())?;
989
990 if !check_ticket_win(
991 self.ticket.verified_hash(),
992 self.ticket.verified_signature(),
993 &self.ticket.win_prob(),
994 &self.response,
995 &vrf_params,
996 ) {
997 return Err(CoreTypesError::TicketNotWinning);
998 }
999
1000 Ok(RedeemableTicket {
1001 ticket: self.ticket,
1002 response: self.response,
1003 vrf_params,
1004 channel_dst: *domain_separator,
1005 })
1006 }
1007
1008 pub fn into_transferable(
1011 self,
1012 chain_keypair: &ChainKeypair,
1013 domain_separator: &Hash,
1014 ) -> errors::Result<TransferableWinningTicket> {
1015 self.into_redeemable(chain_keypair, domain_separator)
1016 .map(TransferableWinningTicket::from)
1017 }
1018}
1019
1020impl Display for AcknowledgedTicket {
1021 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1022 write!(f, "acknowledged {} in state '{}'", self.ticket, self.status)
1023 }
1024}
1025
1026#[derive(Clone, Copy, Debug)]
1028#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1029pub struct RedeemableTicket {
1030 pub ticket: VerifiedTicket,
1032 pub response: Response,
1034 pub vrf_params: VrfParameters,
1036 pub channel_dst: Hash,
1038}
1039
1040impl RedeemableTicket {
1041 #[inline]
1043 pub fn verified_ticket(&self) -> &Ticket {
1044 self.ticket.verified_ticket()
1045 }
1046
1047 #[inline]
1049 pub fn ticket_id(&self) -> TicketId {
1050 TicketId::from(&self.ticket)
1051 }
1052}
1053
1054impl PartialEq for RedeemableTicket {
1055 fn eq(&self, other: &Self) -> bool {
1056 self.ticket == other.ticket && self.channel_dst == other.channel_dst && self.response == other.response
1057 }
1058}
1059
1060impl Eq for RedeemableTicket {}
1061
1062impl PartialOrd for RedeemableTicket {
1063 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1064 Some(self.cmp(other))
1065 }
1066}
1067
1068impl Ord for RedeemableTicket {
1069 fn cmp(&self, other: &Self) -> Ordering {
1070 self.ticket.cmp(&other.ticket)
1071 }
1072}
1073
1074impl Display for RedeemableTicket {
1075 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1076 write!(f, "redeemable {}", self.ticket)
1077 }
1078}
1079
1080impl From<RedeemableTicket> for AcknowledgedTicket {
1081 fn from(value: RedeemableTicket) -> Self {
1082 Self {
1083 status: AcknowledgedTicketStatus::Untouched,
1084 ticket: value.ticket,
1085 response: value.response,
1086 }
1087 }
1088}
1089
1090#[derive(Debug, Copy, Clone)]
1098#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1099pub struct TransferableWinningTicket {
1100 pub ticket: Ticket,
1101 pub response: Response,
1102 pub vrf_params: VrfParameters,
1103 pub signer: Address,
1104}
1105
1106impl TransferableWinningTicket {
1107 pub fn into_redeemable(
1114 self,
1115 expected_issuer: &Address,
1116 domain_separator: &Hash,
1117 ) -> errors::Result<RedeemableTicket> {
1118 if !self.signer.eq(expected_issuer) {
1119 return Err(InvalidInputData("invalid ticket issuer".into()));
1120 }
1121
1122 let verified_ticket = self
1123 .ticket
1124 .verify(&self.signer, domain_separator)
1125 .map_err(|_| CoreTypesError::CryptoError(CryptoError::SignatureVerification))?;
1126
1127 if check_ticket_win(
1128 verified_ticket.verified_hash(),
1129 verified_ticket.verified_signature(),
1130 &verified_ticket.verified_ticket().win_prob(),
1131 &self.response,
1132 &self.vrf_params,
1133 ) {
1134 Ok(RedeemableTicket {
1135 ticket: verified_ticket,
1136 response: self.response,
1137 vrf_params: self.vrf_params,
1138 channel_dst: *domain_separator,
1139 })
1140 } else {
1141 Err(InvalidInputData("ticket is not a win".into()))
1142 }
1143 }
1144}
1145
1146impl PartialEq for TransferableWinningTicket {
1147 fn eq(&self, other: &Self) -> bool {
1148 self.ticket == other.ticket && self.signer == other.signer && self.response == other.response
1149 }
1150}
1151
1152impl From<RedeemableTicket> for TransferableWinningTicket {
1153 fn from(value: RedeemableTicket) -> Self {
1154 Self {
1155 response: value.response,
1156 vrf_params: value.vrf_params,
1157 signer: *value.ticket.verified_issuer(),
1158 ticket: value.ticket.leak(),
1159 }
1160 }
1161}
1162
1163#[cfg(test)]
1164pub mod tests {
1165 use hex_literal::hex;
1166 use hopr_crypto_random::Randomizable;
1167 use hopr_crypto_types::{
1168 keypairs::{ChainKeypair, Keypair},
1169 types::{HalfKey, Hash, Response},
1170 };
1171 use hopr_primitive_types::{
1172 prelude::UnitaryFloatOps,
1173 primitives::{Address, EthereumChallenge, U256},
1174 };
1175
1176 use super::*;
1177
1178 lazy_static::lazy_static! {
1179 static ref ALICE: ChainKeypair = ChainKeypair::from_secret(&hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775")).expect("lazy static keypair should be constructible");
1180 static ref BOB: ChainKeypair = ChainKeypair::from_secret(&hex!("48680484c6fc31bc881a0083e6e32b6dc789f9eaba0f8b981429fd346c697f8c")).expect("lazy static keypair should be constructible");
1181 }
1182
1183 #[cfg(feature = "serde")]
1184 const BINCODE_CONFIGURATION: bincode::config::Configuration = bincode::config::standard()
1185 .with_little_endian()
1186 .with_variable_int_encoding();
1187
1188 #[test]
1189 pub fn test_win_prob_to_f64() -> anyhow::Result<()> {
1190 assert_eq!(0.0f64, WinningProbability::NEVER.as_f64());
1191
1192 assert_eq!(1.0f64, WinningProbability::ALWAYS.as_f64());
1193
1194 let mut test_bit_string = [0xffu8; 7];
1195 test_bit_string[0] = 0x7f;
1196 assert_eq!(0.5f64, WinningProbability::from(&test_bit_string).as_f64());
1197
1198 test_bit_string[0] = 0x3f;
1199 assert_eq!(0.25f64, WinningProbability::from(&test_bit_string).as_f64());
1200
1201 test_bit_string[0] = 0x1f;
1202 assert_eq!(0.125f64, WinningProbability::from(&test_bit_string).as_f64());
1203
1204 Ok(())
1205 }
1206
1207 #[test]
1208 pub fn test_f64_to_win_prob() -> anyhow::Result<()> {
1209 assert_eq!([0u8; 7], WinningProbability::try_from(0.0f64)?.as_encoded());
1210
1211 let mut test_bit_string = [0xffu8; 7];
1212 assert_eq!(test_bit_string, WinningProbability::try_from(1.0f64)?.as_encoded());
1213
1214 test_bit_string[0] = 0x7f;
1215 assert_eq!(test_bit_string, WinningProbability::try_from(0.5f64)?.as_encoded());
1216
1217 test_bit_string[0] = 0x3f;
1218 assert_eq!(test_bit_string, WinningProbability::try_from(0.25f64)?.as_encoded());
1219
1220 test_bit_string[0] = 0x1f;
1221 assert_eq!(test_bit_string, WinningProbability::try_from(0.125f64)?.as_encoded());
1222
1223 Ok(())
1224 }
1225
1226 #[test]
1227 pub fn test_win_prob_approx_eq() -> anyhow::Result<()> {
1228 let wp_0 = WinningProbability(hex!("0020C49BBFFFFF"));
1229 let wp_1 = WinningProbability(hex!("0020C49BA5E34F"));
1230
1231 assert_ne!(wp_0, wp_1.as_encoded());
1233 assert!(!wp_0.lex_eq(&wp_1));
1234
1235 assert_eq!(wp_0, wp_1.as_f64());
1236 assert!(wp_0.approx_eq(&wp_1));
1237
1238 Ok(())
1239 }
1240
1241 #[test]
1242 pub fn test_win_prob_back_and_forth() -> anyhow::Result<()> {
1243 for float in [0.1f64, 0.002f64, 0.00001f64, 0.7311111f64, 1.0f64, 0.0f64] {
1244 assert!((float - WinningProbability::try_from_f64(float)?.as_f64()).abs() < f64::EPSILON);
1245 }
1246
1247 Ok(())
1248 }
1249
1250 #[test]
1251 pub fn test_win_prob_must_be_correctly_approx_ordered() {
1252 let increment = WinningProbability::EPSILON * 100.0; let mut prev = WinningProbability::NEVER;
1254 while let Ok(next) = WinningProbability::try_from_f64(prev.as_f64() + increment) {
1255 assert!(prev.approx_cmp(&next).is_lt());
1256 prev = next;
1257 }
1258 }
1259
1260 #[test]
1261 pub fn test_win_prob_must_be_correctly_lex_ordered() {
1262 let increment = WinningProbability::EPSILON * 100.0; let mut prev = WinningProbability::NEVER;
1264 while let Ok(next) = WinningProbability::try_from_f64(prev.as_f64() + increment) {
1265 assert!(prev.lex_cmp(&next).is_lt());
1266 prev = next;
1267 }
1268 }
1269
1270 #[test]
1271 pub fn test_win_prob_epsilon_must_be_never() -> anyhow::Result<()> {
1272 assert!(WinningProbability::NEVER.approx_eq(&WinningProbability::try_from_f64(WinningProbability::EPSILON)?));
1273 assert!(WinningProbability::NEVER.lex_eq(&WinningProbability::try_from_f64(WinningProbability::EPSILON)?));
1274 Ok(())
1275 }
1276
1277 #[test]
1278 pub fn test_win_prob_bounds_must_be_approx_eq() -> anyhow::Result<()> {
1279 let bound = 0.1 + WinningProbability::EPSILON;
1280 let other = 0.1;
1281 assert!(WinningProbability::try_from_f64(bound)?.approx_eq(&WinningProbability::try_from_f64(other)?));
1282 Ok(())
1283 }
1284
1285 #[test]
1286 pub fn test_win_prob_bounds_must_not_be_approx_eq_when_differ_by_more_then_epsilon() -> anyhow::Result<()> {
1287 let bound = 0.1 + 1.1 * WinningProbability::EPSILON;
1288 let other = 0.1;
1289 assert!(!WinningProbability::try_from_f64(bound)?.approx_eq(&WinningProbability::try_from_f64(other)?));
1290 Ok(())
1291 }
1292
1293 #[test]
1294 pub fn test_win_prob_bounds_must_not_be_lex_eq() -> anyhow::Result<()> {
1295 let bound = 0.1 + WinningProbability::EPSILON;
1296 let other = 0.1;
1297 assert!(!WinningProbability::try_from_f64(bound)?.lex_eq(&WinningProbability::try_from_f64(other)?));
1298 Ok(())
1299 }
1300
1301 #[test]
1302 pub fn test_ticket_builder_zero_hop() -> anyhow::Result<()> {
1303 let ticket = TicketBuilder::zero_hop()
1304 .counterparty(&*BOB)
1305 .eth_challenge(Default::default())
1306 .build()?;
1307 assert_eq!(0, ticket.index);
1308 assert_eq!(0.0, ticket.win_prob().as_f64());
1309 assert_eq!(0, ticket.channel_epoch);
1310
1311 Ok(())
1312 }
1313
1314 #[test]
1315 pub fn test_ticket_serialize_deserialize() -> anyhow::Result<()> {
1316 let initial_ticket = TicketBuilder::default()
1317 .counterparty(&*BOB)
1318 .balance(1.into())
1319 .index(0)
1320 .win_prob(1.0.try_into()?)
1321 .channel_epoch(1)
1322 .eth_challenge(Default::default())
1323 .build_signed(&ALICE, &Default::default())?;
1324
1325 assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
1326
1327 let ticket_bytes: [u8; Ticket::SIZE] = initial_ticket.verified_ticket().clone().into();
1328 assert_eq!(
1329 initial_ticket.verified_ticket(),
1330 &Ticket::try_from(ticket_bytes.as_ref())?
1331 );
1332 Ok(())
1333 }
1334
1335 #[test]
1336 #[cfg(feature = "serde")]
1337 pub fn test_ticket_serialize_deserialize_serde() -> anyhow::Result<()> {
1338 let initial_ticket = TicketBuilder::default()
1339 .counterparty(&*BOB)
1340 .balance(1.into())
1341 .index(0)
1342 .win_prob(1.0.try_into()?)
1343 .channel_epoch(1)
1344 .eth_challenge(Default::default())
1345 .build_signed(&ALICE, &Default::default())?;
1346
1347 assert_eq!(
1348 initial_ticket,
1349 bincode::serde::decode_from_slice(
1350 &bincode::serde::encode_to_vec(&initial_ticket, BINCODE_CONFIGURATION)?,
1351 BINCODE_CONFIGURATION
1352 )
1353 .map(|v| v.0)?
1354 );
1355 Ok(())
1356 }
1357
1358 #[test]
1359 pub fn test_ticket_sign_verify() -> anyhow::Result<()> {
1360 let initial_ticket = TicketBuilder::default()
1361 .counterparty(&*BOB)
1362 .balance(1.into())
1363 .index(0)
1364 .win_prob(1.0.try_into()?)
1365 .channel_epoch(1)
1366 .eth_challenge(Default::default())
1367 .build_signed(&ALICE, &Default::default())?;
1368
1369 assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
1370 assert_eq!(
1371 initial_ticket.channel_id(),
1372 &generate_channel_id(&ALICE.public().to_address(), &BOB.public().to_address())
1373 );
1374
1375 let ticket = initial_ticket.leak();
1376 assert!(ticket.verify(&ALICE.public().to_address(), &Default::default()).is_ok());
1377
1378 Ok(())
1379 }
1380
1381 #[test]
1382 pub fn test_zero_hop() -> anyhow::Result<()> {
1383 let ticket = TicketBuilder::zero_hop()
1384 .counterparty(&*BOB)
1385 .eth_challenge(Default::default())
1386 .build_signed(&ALICE, &Default::default())?;
1387
1388 assert!(
1389 ticket
1390 .leak()
1391 .verify(&ALICE.public().to_address(), &Hash::default())
1392 .is_ok()
1393 );
1394 Ok(())
1395 }
1396
1397 fn mock_ticket(
1398 pk: &ChainKeypair,
1399 counterparty: &Address,
1400 domain_separator: Option<Hash>,
1401 challenge: Option<EthereumChallenge>,
1402 ) -> anyhow::Result<VerifiedTicket> {
1403 let win_prob = 1.0f64; let price_per_packet: U256 = 10000000000000000u128.into(); let path_pos = 5u64;
1406
1407 Ok(TicketBuilder::default()
1408 .counterparty(*counterparty)
1409 .amount(price_per_packet.div_f64(win_prob)? * U256::from(path_pos))
1410 .index(0)
1411 .win_prob(1.0.try_into()?)
1412 .channel_epoch(4)
1413 .eth_challenge(challenge.unwrap_or_default())
1414 .build_signed(pk, &domain_separator.unwrap_or_default())?)
1415 }
1416
1417 #[test]
1418 fn test_unacknowledged_ticket_challenge_response() -> anyhow::Result<()> {
1419 let hk1 = HalfKey::try_from(hex!("3477d7de923ba3a7d5d72a7d6c43fd78395453532d03b2a1e2b9a7cc9b61bafa").as_ref())?;
1420
1421 let hk2 = HalfKey::try_from(hex!("4471496ef88d9a7d86a92b7676f3c8871a60792a37fae6fc3abc347c3aa3b16b").as_ref())?;
1422
1423 let challenge = Response::from_half_keys(&hk1, &hk2)?.to_challenge()?;
1424
1425 let dst = Hash::default();
1426 let ack = mock_ticket(
1427 &ALICE,
1428 &BOB.public().to_address(),
1429 Some(dst),
1430 Some(challenge.to_ethereum_challenge()),
1431 )?
1432 .into_unacknowledged(hk1)
1433 .acknowledge(&hk2)?;
1434
1435 assert!(ack.is_winning(&BOB, &dst), "ticket must be winning");
1436 Ok(())
1437 }
1438
1439 #[test]
1440 #[cfg(feature = "serde")]
1441 fn test_acknowledged_ticket_serde() -> anyhow::Result<()> {
1442 let response =
1443 Response::try_from(hex!("876a41ee5fb2d27ac14d8e8d552692149627c2f52330ba066f9e549aef762f73").as_ref())?;
1444
1445 let dst = Hash::default();
1446
1447 let ticket = mock_ticket(
1448 &ALICE,
1449 &BOB.public().to_address(),
1450 Some(dst),
1451 Some(response.to_challenge()?.to_ethereum_challenge()),
1452 )?;
1453
1454 let acked_ticket = ticket.into_acknowledged(response);
1455
1456 let mut deserialized_ticket = bincode::serde::decode_from_slice(
1457 &bincode::serde::encode_to_vec(&acked_ticket, BINCODE_CONFIGURATION)?,
1458 BINCODE_CONFIGURATION,
1459 )
1460 .map(|v| v.0)?;
1461 assert_eq!(acked_ticket, deserialized_ticket);
1462
1463 assert!(deserialized_ticket.is_winning(&BOB, &dst));
1464
1465 deserialized_ticket.status = AcknowledgedTicketStatus::BeingRedeemed;
1466
1467 assert_eq!(
1468 deserialized_ticket,
1469 bincode::serde::decode_from_slice(
1470 &bincode::serde::encode_to_vec(&deserialized_ticket, BINCODE_CONFIGURATION)?,
1471 BINCODE_CONFIGURATION,
1472 )
1473 .map(|v| v.0)?
1474 );
1475 Ok(())
1476 }
1477
1478 #[test]
1479 fn test_ticket_entire_ticket_transfer_flow() -> anyhow::Result<()> {
1480 let hk1 = HalfKey::random();
1481 let hk2 = HalfKey::random();
1482 let resp = Response::from_half_keys(&hk1, &hk2)?;
1483
1484 let verified = TicketBuilder::default()
1485 .counterparty(&*BOB)
1486 .balance(1.into())
1487 .index(0)
1488 .win_prob(1.0.try_into()?)
1489 .channel_epoch(1)
1490 .challenge(resp.to_challenge()?)
1491 .build_signed(&ALICE, &Default::default())?;
1492
1493 let unack = verified.into_unacknowledged(hk1);
1494 let acknowledged = unack.acknowledge(&hk2).expect("should acknowledge");
1495
1496 let redeemable_1 = acknowledged.clone().into_redeemable(&BOB, &Hash::default())?;
1497
1498 let transferable = acknowledged.into_transferable(&BOB, &Hash::default())?;
1499
1500 let redeemable_2 = transferable.into_redeemable(&ALICE.public().to_address(), &Hash::default())?;
1501
1502 assert_eq!(redeemable_1, redeemable_2);
1503 assert_eq!(redeemable_1.vrf_params.V, redeemable_2.vrf_params.V);
1504 Ok(())
1505 }
1506}