1use ethers::contract::EthCall;
2use hex_literal::hex;
3use hopr_bindings::hopr_channels::RedeemTicketCall;
4use hopr_crypto_types::prelude::*;
5use hopr_primitive_types::prelude::*;
6use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8use std::fmt::{Display, Formatter};
9use tracing::{debug, error};
10
11use crate::errors;
12use crate::errors::CoreTypesError;
13use crate::prelude::CoreTypesError::InvalidInputData;
14use crate::prelude::{generate_channel_id, DEFAULT_OUTGOING_TICKET_WIN_PROB};
15
16const ENCODED_TICKET_LENGTH: usize = 64;
19
20const ENCODED_WIN_PROB_LENGTH: usize = 7;
24
25pub type EncodedWinProb = [u8; ENCODED_WIN_PROB_LENGTH];
27
28pub const ALWAYS_WINNING: EncodedWinProb = hex!("ffffffffffffff");
30
31pub const NEVER_WINNING: EncodedWinProb = hex!("00000000000000");
33
34pub(crate) fn check_ticket_win(
36 ticket_hash: &Hash,
37 ticket_signature: &Signature,
38 win_prob: &EncodedWinProb,
39 response: &Response,
40 vrf_params: &VrfParameters,
41) -> bool {
42 let mut signed_ticket_luck = [0u8; 8];
44 signed_ticket_luck[1..].copy_from_slice(win_prob);
45
46 let mut computed_ticket_luck = [0u8; 8];
48 computed_ticket_luck[1..].copy_from_slice(
49 &Hash::create(&[
50 ticket_hash.as_ref(),
51 &vrf_params.V.as_uncompressed().as_bytes()[1..], response.as_ref(),
53 ticket_signature.as_ref(),
54 ])
55 .as_ref()[0..7],
56 );
57
58 u64::from_be_bytes(computed_ticket_luck) <= u64::from_be_bytes(signed_ticket_luck)
59}
60
61#[derive(Debug, Clone, smart_default::SmartDefault)]
68pub struct TicketBuilder {
69 channel_id: Option<Hash>,
70 amount: Option<U256>,
71 balance: Option<Balance>,
72 #[default = 0]
73 index: u64,
74 #[default = 1]
75 index_offset: u32,
76 #[default = 1]
77 channel_epoch: u32,
78 #[default(Some(DEFAULT_OUTGOING_TICKET_WIN_PROB))]
79 win_prob: Option<f64>,
80 win_prob_enc: Option<EncodedWinProb>,
81 challenge: Option<EthereumChallenge>,
82 signature: Option<Signature>,
83}
84
85impl TicketBuilder {
86 #[must_use]
88 pub fn zero_hop() -> Self {
89 Self {
90 index: 0,
91 amount: Some(U256::zero()),
92 index_offset: 1,
93 win_prob: Some(0.0),
94 channel_epoch: 0,
95 ..Default::default()
96 }
97 }
98
99 #[must_use]
102 pub fn direction(mut self, source: &Address, destination: &Address) -> Self {
103 self.channel_id = Some(generate_channel_id(source, destination));
104 self
105 }
106
107 #[must_use]
110 pub fn addresses<T: Into<Address>, U: Into<Address>>(mut self, source: T, destination: U) -> Self {
111 self.channel_id = Some(generate_channel_id(&source.into(), &destination.into()));
112 self
113 }
114
115 #[must_use]
118 pub fn channel_id(mut self, channel_id: Hash) -> Self {
119 self.channel_id = Some(channel_id);
120 self
121 }
122
123 #[must_use]
126 pub fn amount<T: Into<U256>>(mut self, amount: T) -> Self {
127 self.amount = Some(amount.into());
128 self.balance = None;
129 self
130 }
131
132 #[must_use]
135 pub fn balance(mut self, balance: Balance) -> Self {
136 self.balance = Some(balance);
137 self.amount = None;
138 self
139 }
140
141 #[must_use]
145 pub fn index(mut self, index: u64) -> Self {
146 self.index = index;
147 self
148 }
149
150 #[must_use]
154 pub fn index_offset(mut self, index_offset: u32) -> Self {
155 self.index_offset = index_offset;
156 self
157 }
158
159 #[must_use]
163 pub fn channel_epoch(mut self, channel_epoch: u32) -> Self {
164 self.channel_epoch = channel_epoch;
165 self
166 }
167
168 #[must_use]
172 pub fn win_prob(mut self, win_prob: f64) -> Self {
173 self.win_prob = Some(win_prob);
174 self.win_prob_enc = None;
175 self
176 }
177
178 #[must_use]
182 pub fn win_prob_encoded(mut self, win_prob: EncodedWinProb) -> Self {
183 self.win_prob = None;
184 self.win_prob_enc = Some(win_prob);
185 self
186 }
187
188 #[must_use]
191 pub fn challenge(mut self, challenge: EthereumChallenge) -> Self {
192 self.challenge = Some(challenge);
193 self
194 }
195
196 #[must_use]
199 pub fn signature(mut self, signature: Signature) -> Self {
200 self.signature = Some(signature);
201 self
202 }
203
204 pub fn build(self) -> errors::Result<Ticket> {
208 let amount = match (self.amount, self.balance) {
209 (Some(amount), None) if amount.lt(&10_u128.pow(25).into()) => BalanceType::HOPR.balance(amount),
210 (None, Some(balance))
211 if balance.balance_type() == BalanceType::HOPR && balance.amount().lt(&10_u128.pow(25).into()) =>
212 {
213 balance
214 }
215 (None, None) => return Err(InvalidInputData("missing ticket amount".into())),
216 (Some(_), Some(_)) => {
217 return Err(InvalidInputData(
218 "either amount or balance must be set but not both".into(),
219 ))
220 }
221 _ => {
222 return Err(InvalidInputData(
223 "tickets may not have more than 1% of total supply".into(),
224 ))
225 }
226 };
227
228 if self.index > (1_u64 << 48) {
229 return Err(InvalidInputData("cannot hold ticket indices larger than 2^48".into()));
230 }
231
232 if self.channel_epoch > (1_u32 << 24) {
233 return Err(InvalidInputData("cannot hold channel epoch larger than 2^24".into()));
234 }
235
236 let encoded_win_prob = match (self.win_prob, self.win_prob_enc) {
237 (Some(win_prob), None) => f64_to_win_prob(win_prob)?,
238 (None, Some(win_prob)) => win_prob,
239 (Some(_), Some(_)) => return Err(InvalidInputData("conflicting winning probabilities".into())),
240 (None, None) => return Err(InvalidInputData("missing ticket winning probability".into())),
241 };
242
243 if self.index_offset < 1 {
244 return Err(InvalidInputData(
245 "ticket index offset must be greater or equal to 1".into(),
246 ));
247 }
248
249 Ok(Ticket {
250 channel_id: self.channel_id.ok_or(InvalidInputData("missing channel id".into()))?,
251 amount,
252 index: self.index,
253 index_offset: self.index_offset,
254 encoded_win_prob,
255 channel_epoch: self.channel_epoch,
256 challenge: self
257 .challenge
258 .ok_or(InvalidInputData("missing ticket challenge".into()))?,
259 signature: self.signature,
260 })
261 }
262
263 pub fn build_signed(self, signer: &ChainKeypair, domain_separator: &Hash) -> errors::Result<VerifiedTicket> {
266 if self.signature.is_none() {
267 Ok(self.build()?.sign(signer, domain_separator))
268 } else {
269 Err(InvalidInputData("signature already set".into()))
270 }
271 }
272
273 pub fn build_verified(self, hash: Hash) -> errors::Result<VerifiedTicket> {
278 if let Some(signature) = self.signature {
279 let issuer = PublicKey::from_signature_hash(hash.as_ref(), &signature)?.to_address();
280 Ok(VerifiedTicket(self.build()?, hash, issuer))
281 } else {
282 Err(InvalidInputData("signature is missing".into()))
283 }
284 }
285}
286
287impl From<&Ticket> for TicketBuilder {
288 fn from(value: &Ticket) -> Self {
289 Self {
290 channel_id: Some(value.channel_id),
291 amount: None,
292 balance: Some(value.amount),
293 index: value.index,
294 index_offset: value.index_offset,
295 channel_epoch: value.channel_epoch,
296 win_prob: None,
297 win_prob_enc: Some(value.encoded_win_prob),
298 challenge: Some(value.challenge),
299 signature: None,
300 }
301 }
302}
303
304impl From<Ticket> for TicketBuilder {
305 fn from(value: Ticket) -> Self {
306 Self::from(&value)
307 }
308}
309
310#[cfg_attr(doc, aquamarine::aquamarine)]
311#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
332pub struct Ticket {
333 pub channel_id: Hash,
336 pub amount: Balance, pub index: u64, pub index_offset: u32, pub encoded_win_prob: EncodedWinProb, pub channel_epoch: u32, pub challenge: EthereumChallenge,
353 pub signature: Option<Signature>,
355}
356
357impl PartialOrd for Ticket {
358 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
359 Some(self.cmp(other))
360 }
361}
362
363impl Ord for Ticket {
364 fn cmp(&self, other: &Self) -> Ordering {
365 match self.channel_id.cmp(&other.channel_id) {
368 Ordering::Equal => match self.channel_epoch.cmp(&other.channel_epoch) {
369 Ordering::Equal => self.index.cmp(&other.index),
370 Ordering::Greater => Ordering::Greater,
371 Ordering::Less => Ordering::Less,
372 },
373 Ordering::Greater => Ordering::Greater,
374 Ordering::Less => Ordering::Less,
375 }
376 }
377}
378
379impl Display for Ticket {
380 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
381 write!(
382 f,
383 "ticket #{}, offset {}, epoch {} in channel {}",
384 self.index, self.index_offset, self.channel_epoch, self.channel_id
385 )
386 }
387}
388
389impl Ticket {
390 fn encode_without_signature(&self) -> [u8; Self::SIZE - Signature::SIZE] {
391 let mut ret = [0u8; Self::SIZE - Signature::SIZE];
392 let mut offset = 0;
393
394 ret[offset..offset + Hash::SIZE].copy_from_slice(self.channel_id.as_ref());
395 offset += Hash::SIZE;
396
397 ret[offset..offset + 12].copy_from_slice(&self.amount.amount().to_be_bytes()[20..32]);
399 offset += 12;
400
401 ret[offset..offset + 6].copy_from_slice(&self.index.to_be_bytes()[2..8]);
403 offset += 6;
404
405 ret[offset..offset + 4].copy_from_slice(&self.index_offset.to_be_bytes());
406 offset += 4;
407
408 ret[offset..offset + 3].copy_from_slice(&self.channel_epoch.to_be_bytes()[1..4]);
410 offset += 3;
411
412 ret[offset..offset + ENCODED_WIN_PROB_LENGTH].copy_from_slice(&self.encoded_win_prob);
413 offset += ENCODED_WIN_PROB_LENGTH;
414
415 ret[offset..offset + EthereumChallenge::SIZE].copy_from_slice(self.challenge.as_ref());
416
417 ret
418 }
419
420 pub fn get_hash(&self, domain_separator: &Hash) -> Hash {
423 let ticket_hash = Hash::create(&[self.encode_without_signature().as_ref()]); let hash_struct = Hash::create(&[&RedeemTicketCall::selector(), &[0u8; 28], ticket_hash.as_ref()]);
425 Hash::create(&[&hex!("1901"), domain_separator.as_ref(), hash_struct.as_ref()])
426 }
427
428 pub fn sign(mut self, signing_key: &ChainKeypair, domain_separator: &Hash) -> VerifiedTicket {
431 let ticket_hash = self.get_hash(domain_separator);
432 self.signature = Some(Signature::sign_hash(ticket_hash.as_ref(), signing_key));
433 VerifiedTicket(self, ticket_hash, signing_key.public().to_address())
434 }
435
436 pub fn verify(self, issuer: &Address, domain_separator: &Hash) -> Result<VerifiedTicket, Box<Ticket>> {
444 let ticket_hash = self.get_hash(domain_separator);
445
446 if let Some(signature) = &self.signature {
447 match PublicKey::from_signature_hash(ticket_hash.as_ref(), signature) {
448 Ok(pk) if pk.to_address().eq(issuer) => Ok(VerifiedTicket(self, ticket_hash, *issuer)),
449 Err(e) => {
450 error!("failed to verify ticket signature: {e}");
451 Err(self.into())
452 }
453 _ => Err(self.into()),
454 }
455 } else {
456 Err(self.into())
457 }
458 }
459
460 pub fn is_aggregated(&self) -> bool {
462 self.index_offset > 1
464 }
465
466 pub fn win_prob(&self) -> f64 {
468 win_prob_to_f64(&self.encoded_win_prob)
469 }
470}
471
472impl From<Ticket> for [u8; TICKET_SIZE] {
473 fn from(value: Ticket) -> Self {
474 let mut ret = [0u8; TICKET_SIZE];
475 ret[0..Ticket::SIZE - Signature::SIZE].copy_from_slice(value.encode_without_signature().as_ref());
476 ret[Ticket::SIZE - Signature::SIZE..].copy_from_slice(
477 value
478 .signature
479 .expect("cannot serialize ticket without signature")
480 .as_ref(),
481 );
482 ret
483 }
484}
485
486impl TryFrom<&[u8]> for Ticket {
487 type Error = GeneralError;
488
489 fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
490 if value.len() == Self::SIZE {
491 let mut offset = 0;
492
493 let channel_id = Hash::try_from(&value[offset..offset + Hash::SIZE])?;
495 offset += Hash::SIZE;
496
497 let mut amount = [0u8; 32];
498 amount[20..32].copy_from_slice(&value[offset..offset + 12]);
499 offset += 12;
500
501 let mut index = [0u8; 8];
502 index[2..8].copy_from_slice(&value[offset..offset + 6]);
503 offset += 6;
504
505 let mut index_offset = [0u8; 4];
506 index_offset.copy_from_slice(&value[offset..offset + 4]);
507 offset += 4;
508
509 let mut channel_epoch = [0u8; 4];
510 channel_epoch[1..4].copy_from_slice(&value[offset..offset + 3]);
511 offset += 3;
512
513 let mut encoded_win_prob = [0u8; 7];
514 encoded_win_prob.copy_from_slice(&value[offset..offset + 7]);
515 offset += 7;
516
517 debug_assert_eq!(offset, ENCODED_TICKET_LENGTH);
518
519 let challenge = EthereumChallenge::try_from(&value[offset..offset + EthereumChallenge::SIZE])?;
520 offset += EthereumChallenge::SIZE;
521
522 let signature = Signature::try_from(&value[offset..offset + Signature::SIZE])?;
523
524 TicketBuilder::default()
526 .channel_id(channel_id)
527 .amount(amount)
528 .index(u64::from_be_bytes(index))
529 .index_offset(u32::from_be_bytes(index_offset))
530 .channel_epoch(u32::from_be_bytes(channel_epoch))
531 .win_prob_encoded(encoded_win_prob)
532 .challenge(challenge)
533 .signature(signature)
534 .build()
535 .map_err(|e| GeneralError::ParseError(format!("ticket build failed: {e}")))
536 } else {
537 Err(GeneralError::ParseError("Ticket".into()))
538 }
539 }
540}
541
542const TICKET_SIZE: usize = ENCODED_TICKET_LENGTH + EthereumChallenge::SIZE + Signature::SIZE;
543
544impl BytesEncodable<TICKET_SIZE> for Ticket {}
545
546#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
550pub struct VerifiedTicket(Ticket, Hash, Address);
551
552impl VerifiedTicket {
553 pub fn win_prob(&self) -> f64 {
555 self.0.win_prob()
556 }
557
558 pub fn is_winning(&self, response: &Response, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
576 if let Ok(vrf_params) = derive_vrf_parameters(self.1, chain_keypair, domain_separator.as_ref()) {
577 check_ticket_win(
578 &self.1,
579 self.0
580 .signature
581 .as_ref()
582 .expect("verified ticket have always a signature"),
583 &self.0.encoded_win_prob,
584 response,
585 &vrf_params,
586 )
587 } else {
588 error!("cannot derive vrf parameters for {self}");
589 false
590 }
591 }
592
593 pub fn get_path_position(&self, price_per_packet: U256) -> errors::Result<u8> {
601 let pos = self.0.amount.amount() / price_per_packet.div_f64(self.win_prob())?;
602 pos.as_u64()
603 .try_into() .map_err(|_| CoreTypesError::ArithmeticError(format!("Cannot convert {pos} to u8")))
605 }
606
607 pub fn verified_ticket(&self) -> &Ticket {
609 &self.0
610 }
611
612 pub fn verified_hash(&self) -> &Hash {
615 &self.1
616 }
617
618 pub fn verified_issuer(&self) -> &Address {
622 &self.2
623 }
624
625 pub fn verified_signature(&self) -> &Signature {
627 self.0
628 .signature
629 .as_ref()
630 .expect("verified ticket always has a signature")
631 }
632
633 pub fn leak(self) -> Ticket {
635 self.0
636 }
637
638 pub fn into_unacknowledged(self, own_key: HalfKey) -> UnacknowledgedTicket {
641 UnacknowledgedTicket { ticket: self, own_key }
642 }
643
644 pub fn into_acknowledged(self, response: Response) -> AcknowledgedTicket {
647 AcknowledgedTicket {
648 status: AcknowledgedTicketStatus::Untouched,
649 ticket: self,
650 response,
651 }
652 }
653}
654
655impl Display for VerifiedTicket {
656 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
657 write!(f, "verified {}", self.0)
658 }
659}
660
661impl PartialOrd for VerifiedTicket {
662 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
663 Some(self.cmp(other))
664 }
665}
666
667impl Ord for VerifiedTicket {
668 fn cmp(&self, other: &Self) -> Ordering {
669 self.0.cmp(&other.0)
670 }
671}
672
673pub fn win_prob_to_f64(encoded_win_prob: &EncodedWinProb) -> f64 {
675 if encoded_win_prob.eq(&NEVER_WINNING) {
676 return 0.0;
677 }
678
679 if encoded_win_prob.eq(&ALWAYS_WINNING) {
680 return 1.0;
681 }
682
683 let mut tmp = [0u8; 8];
684 tmp[1..].copy_from_slice(encoded_win_prob);
685
686 let tmp = u64::from_be_bytes(tmp);
687
688 let significand: u64 = tmp + 1;
690
691 f64::from_bits((1023u64 << 52) | (significand >> 4)) - 1.0
692}
693
694pub fn f64_to_win_prob(win_prob: f64) -> errors::Result<EncodedWinProb> {
696 if !(0.0..=1.0).contains(&win_prob) {
697 return Err(CoreTypesError::InvalidInputData(
698 "Winning probability must be in [0.0, 1.0]".into(),
699 ));
700 }
701
702 if win_prob == 0.0 {
703 return Ok(NEVER_WINNING);
704 }
705
706 if win_prob == 1.0 {
707 return Ok(ALWAYS_WINNING);
708 }
709
710 let tmp: u64 = (win_prob + 1.0).to_bits();
711
712 let significand: u64 = tmp & 0x000fffffffffffffu64;
714
715 let encoded = ((significand - 1) << 4) | 0x000000000000000fu64;
717
718 let mut res = [0u8; 7];
719 res.copy_from_slice(&encoded.to_be_bytes()[1..]);
720
721 Ok(res)
722}
723
724#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
728pub struct UnacknowledgedTicket {
729 pub ticket: VerifiedTicket,
730 pub(crate) own_key: HalfKey,
731}
732
733impl UnacknowledgedTicket {
734 #[inline]
736 pub fn verified_ticket(&self) -> &Ticket {
737 self.ticket.verified_ticket()
738 }
739
740 pub fn acknowledge(self, acknowledgement: &HalfKey) -> crate::errors::Result<AcknowledgedTicket> {
744 let response = Response::from_half_keys(&self.own_key, acknowledgement)?;
745 debug!("acknowledging ticket using response {}", response.to_hex());
746
747 if self.ticket.verified_ticket().challenge == response.to_challenge().into() {
748 Ok(self.ticket.into_acknowledged(response))
749 } else {
750 Err(CryptoError::InvalidChallenge.into())
751 }
752 }
753}
754
755#[repr(u8)]
757#[derive(
758 Clone,
759 Copy,
760 Debug,
761 Default,
762 Eq,
763 PartialEq,
764 Serialize,
765 Deserialize,
766 strum::Display,
767 strum::EnumString,
768 num_enum::IntoPrimitive,
769 num_enum::TryFromPrimitive,
770)]
771#[strum(serialize_all = "PascalCase")]
772pub enum AcknowledgedTicketStatus {
773 #[default]
775 Untouched = 0,
776 BeingRedeemed = 1,
778 BeingAggregated = 2,
780}
781
782#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
784pub struct AcknowledgedTicket {
785 #[serde(default)]
786 pub status: AcknowledgedTicketStatus,
787 pub ticket: VerifiedTicket,
788 pub response: Response,
789}
790
791impl PartialOrd for AcknowledgedTicket {
792 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
793 Some(self.cmp(other))
794 }
795}
796
797impl Ord for AcknowledgedTicket {
798 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
799 self.ticket.cmp(&other.ticket)
800 }
801}
802
803impl AcknowledgedTicket {
804 #[inline]
806 pub fn verified_ticket(&self) -> &Ticket {
807 self.ticket.verified_ticket()
808 }
809
810 pub fn is_winning(&self, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
812 self.ticket.is_winning(&self.response, chain_keypair, domain_separator)
813 }
814
815 pub fn into_redeemable(
819 self,
820 chain_keypair: &ChainKeypair,
821 domain_separator: &Hash,
822 ) -> crate::errors::Result<RedeemableTicket> {
823 if chain_keypair.public().to_address().eq(self.ticket.verified_issuer()) {
825 return Err(errors::CoreTypesError::LoopbackTicket);
826 }
827
828 let vrf_params = derive_vrf_parameters(self.ticket.verified_hash(), chain_keypair, domain_separator.as_ref())?;
829
830 Ok(RedeemableTicket {
831 ticket: self.ticket,
832 response: self.response,
833 vrf_params,
834 channel_dst: *domain_separator,
835 })
836 }
837
838 pub fn into_transferable(
841 self,
842 chain_keypair: &ChainKeypair,
843 domain_separator: &Hash,
844 ) -> errors::Result<TransferableWinningTicket> {
845 self.into_redeemable(chain_keypair, domain_separator)
846 .map(TransferableWinningTicket::from)
847 }
848}
849
850impl Display for AcknowledgedTicket {
851 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
852 write!(f, "acknowledged {} in state '{}'", self.ticket, self.status)
853 }
854}
855
856#[derive(Clone, Debug, Serialize, Deserialize)]
858pub struct RedeemableTicket {
859 pub ticket: VerifiedTicket,
861 pub response: Response,
863 pub vrf_params: VrfParameters,
865 pub channel_dst: Hash,
867}
868
869impl RedeemableTicket {
870 #[inline]
872 pub fn verified_ticket(&self) -> &Ticket {
873 self.ticket.verified_ticket()
874 }
875}
876
877impl PartialEq for RedeemableTicket {
878 fn eq(&self, other: &Self) -> bool {
879 self.ticket == other.ticket && self.channel_dst == other.channel_dst && self.response == other.response
880 }
881}
882
883impl Display for RedeemableTicket {
884 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
885 write!(f, "redeemable {}", self.ticket)
886 }
887}
888
889impl From<RedeemableTicket> for AcknowledgedTicket {
890 fn from(value: RedeemableTicket) -> Self {
891 Self {
892 status: AcknowledgedTicketStatus::Untouched,
893 ticket: value.ticket,
894 response: value.response,
895 }
896 }
897}
898
899#[derive(Debug, Clone, Serialize, Deserialize)]
907pub struct TransferableWinningTicket {
908 pub ticket: Ticket,
909 pub response: Response,
910 pub vrf_params: VrfParameters,
911 pub signer: Address,
912}
913
914impl TransferableWinningTicket {
915 pub fn into_redeemable(
922 self,
923 expected_issuer: &Address,
924 domain_separator: &Hash,
925 ) -> errors::Result<RedeemableTicket> {
926 if !self.signer.eq(expected_issuer) {
927 return Err(crate::errors::CoreTypesError::InvalidInputData(
928 "invalid ticket issuer".into(),
929 ));
930 }
931
932 let verified_ticket = self
933 .ticket
934 .verify(&self.signer, domain_separator)
935 .map_err(|_| CoreTypesError::CryptoError(CryptoError::SignatureVerification))?;
936
937 if check_ticket_win(
938 verified_ticket.verified_hash(),
939 verified_ticket.verified_signature(),
940 &verified_ticket.verified_ticket().encoded_win_prob,
941 &self.response,
942 &self.vrf_params,
943 ) {
944 Ok(RedeemableTicket {
945 ticket: verified_ticket,
946 response: self.response,
947 vrf_params: self.vrf_params,
948 channel_dst: *domain_separator,
949 })
950 } else {
951 Err(crate::errors::CoreTypesError::InvalidInputData(
952 "ticket is not a win".into(),
953 ))
954 }
955 }
956}
957
958impl PartialEq for TransferableWinningTicket {
959 fn eq(&self, other: &Self) -> bool {
960 self.ticket == other.ticket && self.signer == other.signer && self.response == other.response
961 }
962}
963
964impl PartialOrd<Self> for TransferableWinningTicket {
965 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
966 Some(self.ticket.cmp(&other.ticket))
967 }
968}
969
970impl From<RedeemableTicket> for TransferableWinningTicket {
971 fn from(value: RedeemableTicket) -> Self {
972 Self {
973 response: value.response,
974 vrf_params: value.vrf_params,
975 signer: *value.ticket.verified_issuer(),
976 ticket: value.ticket.leak(),
977 }
978 }
979}
980
981#[cfg(test)]
982pub mod tests {
983 use super::*;
984 use crate::prelude::LOWEST_POSSIBLE_WINNING_PROB;
985 use hex_literal::hex;
986 use hopr_crypto_types::{
987 keypairs::{ChainKeypair, Keypair},
988 types::{Challenge, CurvePoint, HalfKey, Hash, Response},
989 };
990 use hopr_primitive_types::prelude::UnitaryFloatOps;
991 use hopr_primitive_types::primitives::{Address, BalanceType, EthereumChallenge, U256};
992
993 lazy_static::lazy_static! {
994 static ref ALICE: ChainKeypair = ChainKeypair::from_secret(&hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775")).expect("lazy static keypair should be constructible");
995 static ref BOB: ChainKeypair = ChainKeypair::from_secret(&hex!("48680484c6fc31bc881a0083e6e32b6dc789f9eaba0f8b981429fd346c697f8c")).expect("lazy static keypair should be constructible");
996 }
997
998 const BINCODE_CONFIGURATION: bincode::config::Configuration = bincode::config::standard()
999 .with_little_endian()
1000 .with_variable_int_encoding();
1001
1002 #[test]
1003 pub fn test_win_prob_to_f64() {
1004 let mut test_bit_string = [0xffu8; 7];
1005
1006 assert_eq!(0.0f64, super::win_prob_to_f64(&[0u8; 7]));
1007
1008 assert_eq!(1.0f64, super::win_prob_to_f64(&test_bit_string));
1009
1010 test_bit_string[0] = 0x7f;
1011 assert_eq!(0.5f64, super::win_prob_to_f64(&test_bit_string));
1012
1013 test_bit_string[0] = 0x3f;
1014 assert_eq!(0.25f64, super::win_prob_to_f64(&test_bit_string));
1015
1016 test_bit_string[0] = 0x1f;
1017 assert_eq!(0.125f64, super::win_prob_to_f64(&test_bit_string));
1018 }
1019
1020 #[test]
1021 pub fn test_f64_to_win_prob() -> anyhow::Result<()> {
1022 let mut test_bit_string = [0xffu8; 7];
1023
1024 assert_eq!([0u8; 7], super::f64_to_win_prob(0.0f64)?);
1025
1026 assert_eq!(test_bit_string, super::f64_to_win_prob(1.0f64)?);
1027
1028 test_bit_string[0] = 0x7f;
1029 assert_eq!(test_bit_string, super::f64_to_win_prob(0.5f64)?);
1030
1031 test_bit_string[0] = 0x3f;
1032 assert_eq!(test_bit_string, super::f64_to_win_prob(0.25f64)?);
1033
1034 test_bit_string[0] = 0x1f;
1035 assert_eq!(test_bit_string, super::f64_to_win_prob(0.125f64)?);
1036
1037 Ok(())
1038 }
1039
1040 #[test]
1041 pub fn test_win_prob_approx_eq() {
1042 let wp_0 = win_prob_to_f64(&hex!("0020C49BBFFFFF"));
1043 let wp_1 = win_prob_to_f64(&hex!("0020C49BA5E34F"));
1044
1045 assert_ne!(wp_0, wp_1);
1046 assert!(f64_approx_eq(wp_0, wp_1, LOWEST_POSSIBLE_WINNING_PROB));
1047 }
1048
1049 #[test]
1050 pub fn test_win_prob_back_and_forth() -> anyhow::Result<()> {
1051 for float in [0.1f64, 0.002f64, 0.00001f64, 0.7311111f64, 1.0f64, 0.0f64] {
1052 assert!((float - super::win_prob_to_f64(&super::f64_to_win_prob(float)?)).abs() < f64::EPSILON);
1053 }
1054
1055 Ok(())
1056 }
1057
1058 #[test]
1059 pub fn test_ticket_builder_zero_hop() -> anyhow::Result<()> {
1060 let ticket = TicketBuilder::zero_hop()
1061 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1062 .challenge(Default::default())
1063 .build()?;
1064 assert_eq!(0, ticket.index);
1065 assert_eq!(0.0, ticket.win_prob());
1066 assert_eq!(0, ticket.channel_epoch);
1067 assert_eq!(
1068 generate_channel_id(&ALICE.public().to_address(), &BOB.public().to_address()),
1069 ticket.channel_id
1070 );
1071 Ok(())
1072 }
1073
1074 #[test]
1075 pub fn test_ticket_serialize_deserialize() -> anyhow::Result<()> {
1076 let initial_ticket = TicketBuilder::default()
1077 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1078 .balance(BalanceType::HOPR.one())
1079 .index(0)
1080 .index_offset(1)
1081 .win_prob(1.0)
1082 .channel_epoch(1)
1083 .challenge(Default::default())
1084 .build_signed(&ALICE, &Default::default())?;
1085
1086 assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
1087
1088 let ticket_bytes: [u8; Ticket::SIZE] = initial_ticket.verified_ticket().clone().into();
1089 assert_eq!(
1090 initial_ticket.verified_ticket(),
1091 &Ticket::try_from(ticket_bytes.as_ref())?
1092 );
1093 Ok(())
1094 }
1095
1096 #[test]
1097 pub fn test_ticket_serialize_deserialize_serde() -> anyhow::Result<()> {
1098 let initial_ticket = TicketBuilder::default()
1099 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1100 .balance(BalanceType::HOPR.one())
1101 .index(0)
1102 .index_offset(1)
1103 .win_prob(1.0)
1104 .channel_epoch(1)
1105 .challenge(Default::default())
1106 .build_signed(&ALICE, &Default::default())?;
1107
1108 assert_eq!(
1109 initial_ticket,
1110 bincode::serde::decode_from_slice(
1111 &bincode::serde::encode_to_vec(&initial_ticket, BINCODE_CONFIGURATION)?,
1112 BINCODE_CONFIGURATION
1113 )
1114 .map(|v| v.0)?
1115 );
1116 Ok(())
1117 }
1118
1119 #[test]
1120 pub fn test_ticket_sign_verify() -> anyhow::Result<()> {
1121 let initial_ticket = TicketBuilder::default()
1122 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1123 .balance(BalanceType::HOPR.one())
1124 .index(0)
1125 .index_offset(1)
1126 .win_prob(1.0)
1127 .channel_epoch(1)
1128 .challenge(Default::default())
1129 .build_signed(&ALICE, &Default::default())?;
1130
1131 assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
1132
1133 let ticket = initial_ticket.leak();
1134 assert!(ticket.verify(&ALICE.public().to_address(), &Default::default()).is_ok());
1135 Ok(())
1136 }
1137
1138 #[test]
1139 pub fn test_path_position() -> anyhow::Result<()> {
1140 let builder = TicketBuilder::default()
1141 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1142 .balance(BalanceType::HOPR.one())
1143 .index(0)
1144 .index_offset(1)
1145 .win_prob(1.0)
1146 .channel_epoch(1)
1147 .challenge(Default::default());
1148
1149 let ticket = builder.clone().build_signed(&ALICE, &Default::default())?;
1150
1151 assert_eq!(1u8, ticket.get_path_position(1_u32.into())?);
1152
1153 let ticket = builder
1154 .clone()
1155 .amount(34_u64)
1156 .build_signed(&ALICE, &Default::default())?;
1157
1158 assert_eq!(2u8, ticket.get_path_position(17_u64.into())?);
1159
1160 let ticket = builder
1161 .clone()
1162 .amount(30_u64)
1163 .win_prob(0.2)
1164 .build_signed(&ALICE, &Default::default())?;
1165
1166 assert_eq!(2u8, ticket.get_path_position(3_u64.into())?);
1167 Ok(())
1168 }
1169
1170 #[test]
1171 pub fn test_path_position_mismatch() -> anyhow::Result<()> {
1172 let ticket = TicketBuilder::default()
1173 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1174 .amount(256)
1175 .index(0)
1176 .index_offset(1)
1177 .win_prob(1.0)
1178 .channel_epoch(1)
1179 .challenge(Default::default())
1180 .build_signed(&ALICE, &Default::default())?;
1181
1182 assert!(ticket.get_path_position(1_u64.into()).is_err());
1183 Ok(())
1184 }
1185
1186 #[test]
1187 pub fn test_zero_hop() -> anyhow::Result<()> {
1188 let ticket = TicketBuilder::zero_hop()
1189 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1190 .challenge(Default::default())
1191 .build_signed(&ALICE, &Default::default())?;
1192
1193 assert!(ticket
1194 .leak()
1195 .verify(&ALICE.public().to_address(), &Hash::default())
1196 .is_ok());
1197 Ok(())
1198 }
1199
1200 fn mock_ticket(
1201 pk: &ChainKeypair,
1202 counterparty: &Address,
1203 domain_separator: Option<Hash>,
1204 challenge: Option<EthereumChallenge>,
1205 ) -> anyhow::Result<VerifiedTicket> {
1206 let win_prob = 1.0f64; let price_per_packet: U256 = 10000000000000000u128.into(); let path_pos = 5u64;
1209
1210 Ok(TicketBuilder::default()
1211 .direction(&pk.public().to_address(), counterparty)
1212 .amount(price_per_packet.div_f64(win_prob)? * U256::from(path_pos))
1213 .index(0)
1214 .index_offset(1)
1215 .win_prob(1.0)
1216 .channel_epoch(4)
1217 .challenge(challenge.unwrap_or_default())
1218 .build_signed(pk, &domain_separator.unwrap_or_default())?)
1219 }
1220
1221 #[test]
1222 fn test_unacknowledged_ticket_challenge_response() -> anyhow::Result<()> {
1223 let hk1 = HalfKey::try_from(hex!("3477d7de923ba3a7d5d72a7d6c43fd78395453532d03b2a1e2b9a7cc9b61bafa").as_ref())?;
1224
1225 let hk2 = HalfKey::try_from(hex!("4471496ef88d9a7d86a92b7676f3c8871a60792a37fae6fc3abc347c3aa3b16b").as_ref())?;
1226
1227 let cp1: CurvePoint = hk1.to_challenge().try_into()?;
1228 let cp2: CurvePoint = hk2.to_challenge().try_into()?;
1229 let cp_sum = CurvePoint::combine(&[&cp1, &cp2]);
1230
1231 let dst = Hash::default();
1232 let ack = mock_ticket(
1233 &ALICE,
1234 &BOB.public().to_address(),
1235 Some(dst),
1236 Some(Challenge::from(cp_sum).to_ethereum_challenge()),
1237 )?
1238 .into_unacknowledged(hk1)
1239 .acknowledge(&hk2)?;
1240
1241 assert!(ack.is_winning(&BOB, &dst), "ticket must be winning");
1242 Ok(())
1243 }
1244
1245 #[test]
1246 fn test_acknowledged_ticket() -> anyhow::Result<()> {
1247 let response =
1248 Response::try_from(hex!("876a41ee5fb2d27ac14d8e8d552692149627c2f52330ba066f9e549aef762f73").as_ref())?;
1249
1250 let dst = Hash::default();
1251
1252 let ticket = mock_ticket(
1253 &ALICE,
1254 &BOB.public().to_address(),
1255 Some(dst),
1256 Some(response.to_challenge().into()),
1257 )?;
1258
1259 let acked_ticket = ticket.into_acknowledged(response);
1260
1261 let mut deserialized_ticket = bincode::serde::decode_from_slice(
1262 &bincode::serde::encode_to_vec(&acked_ticket, BINCODE_CONFIGURATION)?,
1263 BINCODE_CONFIGURATION,
1264 )
1265 .map(|v| v.0)?;
1266 assert_eq!(acked_ticket, deserialized_ticket);
1267
1268 assert!(deserialized_ticket.is_winning(&BOB, &dst));
1269
1270 deserialized_ticket.status = super::AcknowledgedTicketStatus::BeingAggregated;
1271
1272 assert_eq!(
1273 deserialized_ticket,
1274 bincode::serde::decode_from_slice(
1275 &bincode::serde::encode_to_vec(&deserialized_ticket, BINCODE_CONFIGURATION)?,
1276 BINCODE_CONFIGURATION,
1277 )
1278 .map(|v| v.0)?
1279 );
1280 Ok(())
1281 }
1282
1283 #[test]
1284 fn test_ticket_entire_ticket_transfer_flow() -> anyhow::Result<()> {
1285 let hk1 = HalfKey::random();
1286 let hk2 = HalfKey::random();
1287 let resp = Response::from_half_keys(&hk1, &hk2)?;
1288
1289 let verified = TicketBuilder::default()
1290 .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1291 .balance(BalanceType::HOPR.one())
1292 .index(0)
1293 .index_offset(1)
1294 .win_prob(1.0)
1295 .channel_epoch(1)
1296 .challenge(resp.to_challenge().to_ethereum_challenge())
1297 .build_signed(&ALICE, &Default::default())?;
1298
1299 let unack = verified.into_unacknowledged(hk1);
1300 let acknowledged = unack.acknowledge(&hk2).expect("should acknowledge");
1301
1302 let redeemable_1 = acknowledged.clone().into_redeemable(&BOB, &Hash::default())?;
1303
1304 let transferable = acknowledged.into_transferable(&BOB, &Hash::default())?;
1305
1306 let redeemable_2 = transferable.into_redeemable(&ALICE.public().to_address(), &Hash::default())?;
1307
1308 assert_eq!(redeemable_1, redeemable_2);
1309 assert_eq!(redeemable_1.vrf_params.V, redeemable_2.vrf_params.V);
1310 Ok(())
1311 }
1312}