hopr_internal_types/
tickets.rs

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
17/// Size-optimized encoding of the ticket, used for both,
18/// network transfer and in the smart contract.
19const ENCODED_TICKET_LENGTH: usize = 64;
20
21/// Custom float to integer encoding used in the integer-only
22/// Ethereum Virtual Machine (EVM). Chosen to be easily
23/// convertible to IEEE754 double-precision and vice versa
24const ENCODED_WIN_PROB_LENGTH: usize = 7;
25
26/// Define the selector for the redeemTicketCall to avoid importing
27/// the entire hopr-bindings crate for one single constant.
28/// This value should be updated with the function interface changes.
29pub const REDEEM_CALL_SELECTOR: [u8; 4] = [252, 183, 121, 111];
30
31/// Winning probability encoded in 7-byte representation
32pub type EncodedWinProb = [u8; ENCODED_WIN_PROB_LENGTH];
33
34/// Represents a ticket winning probability.
35///
36/// It holds the modified IEEE-754 but behaves like a reduced precision float.
37/// It intentionally does not implement `Ord` or `Eq`, as
38/// it can be only [approximately compared](WinningProbability::approx_cmp).
39#[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    /// 100% winning probability
45    pub const ALWAYS: Self = Self([0xff; ENCODED_WIN_PROB_LENGTH]);
46    // This value can no longer be represented with the winning probability encoding
47    // and is equal to 0
48    pub const EPSILON: f64 = 0.00000001;
49    /// 0% winning probability.
50    pub const NEVER: Self = Self([0u8; ENCODED_WIN_PROB_LENGTH]);
51
52    /// Converts winning probability to an unsigned integer (luck).
53    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    /// Convenience function to convert to internal probability representation.
60    pub fn as_encoded(&self) -> EncodedWinProb {
61        self.0
62    }
63
64    /// Convert probability to a float.
65    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        // project interval [0x0fffffffffffff, 0x0000000000000f] to [0x00000000000010, 0x10000000000000]
80        let significand: u64 = tmp + 1;
81
82        f64::from_bits((1023u64 << 52) | (significand >> 4)) - 1.0
83    }
84
85    /// Tries to get probability from a float.
86    pub fn try_from_f64(win_prob: f64) -> errors::Result<Self> {
87        // Also makes sure the input value is not NaN or infinite.
88        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        // // clear sign and exponent
103        let significand: u64 = tmp & 0x000fffffffffffffu64;
104
105        // project interval [0x10000000000000, 0x00000000000010] to [0x0000000000000f, 0x0fffffffffffff]
106        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    /// Performs approximate comparison up to [`Self::EPSILON`].
115    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    /// Performs approximate equality comparison up to [`Self::EPSILON`].
126    pub fn approx_eq(&self, other: &Self) -> bool {
127        self.approx_cmp(other) == Ordering::Equal
128    }
129
130    /// Gets the minimum of two winning probabilities.
131    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    /// Gets the maximum of two winning probabilities.
140    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
234/// Helper function checks if the given ticket values belong to a winning ticket.
235///
236/// This function is inexpensive to compute.
237pub(crate) fn check_ticket_win(
238    ticket_hash: &Hash,
239    ticket_signature: &Signature,
240    win_prob: &WinningProbability,
241    response: &Response,
242    vrf_params: &VrfParameters,
243) -> bool {
244    // Computed winning probability
245    let mut computed_ticket_luck = [0u8; 8];
246    computed_ticket_luck[1..].copy_from_slice(
247        &Hash::create(&[
248            ticket_hash.as_ref(),
249            &vrf_params.get_v_encoded_point().as_bytes()[1..], // skip prefix
250            response.as_ref(),
251            ticket_signature.as_ref(),
252        ])
253        .as_ref()[0..7],
254    );
255
256    u64::from_be_bytes(computed_ticket_luck) <= win_prob.as_luck()
257}
258
259/// Builder for [Ticket] and [VerifiedTicket].
260///
261/// A new builder is created via [TicketBuilder::default] or [TicketBuilder::zero_hop].
262///
263/// Input validation is performed upon calling [TicketBuilder::build], [TicketBuilder::build_signed]
264/// and [TicketBuilder::build_verified].
265#[derive(Debug, Clone, smart_default::SmartDefault)]
266pub struct TicketBuilder {
267    channel_id: Option<Hash>,
268    amount: Option<U256>,
269    balance: Option<HoprBalance>,
270    #[default = 0]
271    index: u64,
272    #[default = 1]
273    index_offset: u32,
274    #[default = 1]
275    channel_epoch: u32,
276    win_prob: WinningProbability,
277    challenge: Option<EthereumChallenge>,
278    signature: Option<Signature>,
279}
280
281impl TicketBuilder {
282    /// Initializes the builder for a zero-hop ticket.
283    #[must_use]
284    pub fn zero_hop() -> Self {
285        Self {
286            index: 0,
287            amount: Some(U256::zero()),
288            index_offset: 1,
289            win_prob: WinningProbability::NEVER,
290            channel_epoch: 0,
291            ..Default::default()
292        }
293    }
294
295    /// Sets channel id based on the `source` and `destination`.
296    /// This, [TicketBuilder::channel_id] or [TicketBuilder::addresses] must be set.
297    #[must_use]
298    pub fn direction(mut self, source: &Address, destination: &Address) -> Self {
299        self.channel_id = Some(generate_channel_id(source, destination));
300        self
301    }
302
303    /// Sets channel id based on the `source` and `destination`.
304    /// This, [TicketBuilder::channel_id] or [TicketBuilder::direction] must be set.
305    #[must_use]
306    pub fn addresses<T: Into<Address>, U: Into<Address>>(mut self, source: T, destination: U) -> Self {
307        self.channel_id = Some(generate_channel_id(&source.into(), &destination.into()));
308        self
309    }
310
311    /// Sets the channel id.
312    /// This, [TicketBuilder::addresses] or [TicketBuilder::direction] must be set.
313    #[must_use]
314    pub fn channel_id(mut self, channel_id: Hash) -> Self {
315        self.channel_id = Some(channel_id);
316        self
317    }
318
319    /// Sets the ticket amount.
320    /// This or [TicketBuilder::balance] must be set and be less or equal to 10^25.
321    #[must_use]
322    pub fn amount<T: Into<U256>>(mut self, amount: T) -> Self {
323        self.amount = Some(amount.into());
324        self.balance = None;
325        self
326    }
327
328    /// Sets the ticket amount as HOPR balance.
329    /// This or [TicketBuilder::amount] must be set and be less or equal to 10^25.
330    #[must_use]
331    pub fn balance(mut self, balance: HoprBalance) -> Self {
332        self.balance = Some(balance);
333        self.amount = None;
334        self
335    }
336
337    /// Sets the ticket index.
338    /// Must be less or equal to 2^48.
339    /// Defaults to 0.
340    #[must_use]
341    pub fn index(mut self, index: u64) -> Self {
342        self.index = index;
343        self
344    }
345
346    /// Sets the index offset.
347    /// Must be greater or equal 1.
348    /// Defaults to 1.
349    #[must_use]
350    pub fn index_offset(mut self, index_offset: u32) -> Self {
351        self.index_offset = index_offset;
352        self
353    }
354
355    /// Sets the channel epoch.
356    /// Must be less or equal to 2^24.
357    /// Defaults to 1.
358    #[must_use]
359    pub fn channel_epoch(mut self, channel_epoch: u32) -> Self {
360        self.channel_epoch = channel_epoch;
361        self
362    }
363
364    /// Sets the ticket winning probability.
365    /// Defaults to 1.0
366    #[must_use]
367    pub fn win_prob(mut self, win_prob: WinningProbability) -> Self {
368        self.win_prob = win_prob;
369        self
370    }
371
372    /// Sets the [`Challenge`] for the Proof of Relay, converting it to [`EthereumChallenge`] first.
373    ///
374    /// Either this method or [`Ticket::eth_challenge`] must be called.
375    #[must_use]
376    pub fn challenge(mut self, challenge: Challenge) -> Self {
377        self.challenge = Some(challenge.to_ethereum_challenge());
378        self
379    }
380
381    /// Sets the [`EthereumChallenge`] for the Proof of Relay.
382    /// Either this method or [`Ticket::challenge`] must be called.
383    pub fn eth_challenge(mut self, challenge: EthereumChallenge) -> Self {
384        self.challenge = Some(challenge);
385        self
386    }
387
388    /// Set the signature of this ticket.
389    /// Defaults to `None`.
390    #[must_use]
391    pub fn signature(mut self, signature: Signature) -> Self {
392        self.signature = Some(signature);
393        self
394    }
395
396    /// Verifies all inputs and builds the [Ticket].
397    /// This **does not** perform signature verification if a [signature](TicketBuilder::signature)
398    /// was set.
399    pub fn build(self) -> errors::Result<Ticket> {
400        let amount = match (self.amount, self.balance) {
401            (Some(amount), None) if amount.lt(&10_u128.pow(25).into()) => HoprBalance::from(amount),
402            (None, Some(balance)) if balance.amount().lt(&10_u128.pow(25).into()) => balance,
403            (None, None) => return Err(InvalidInputData("missing ticket amount".into())),
404            (Some(_), Some(_)) => {
405                return Err(InvalidInputData(
406                    "either amount or balance must be set but not both".into(),
407                ));
408            }
409            _ => {
410                return Err(InvalidInputData(
411                    "tickets may not have more than 1% of total supply".into(),
412                ));
413            }
414        };
415
416        if self.index > (1_u64 << 48) {
417            return Err(InvalidInputData("cannot hold ticket indices larger than 2^48".into()));
418        }
419
420        if self.channel_epoch > (1_u32 << 24) {
421            return Err(InvalidInputData("cannot hold channel epoch larger than 2^24".into()));
422        }
423
424        if self.index_offset < 1 {
425            return Err(InvalidInputData(
426                "ticket index offset must be greater or equal to 1".into(),
427            ));
428        }
429
430        Ok(Ticket {
431            channel_id: self.channel_id.ok_or(InvalidInputData("missing channel id".into()))?,
432            amount,
433            index: self.index,
434            index_offset: self.index_offset,
435            encoded_win_prob: self.win_prob.into(),
436            channel_epoch: self.channel_epoch,
437            challenge: self
438                .challenge
439                .ok_or(InvalidInputData("missing ticket challenge".into()))?,
440            signature: self.signature,
441        })
442    }
443
444    /// Validates all inputs and builds the [VerifiedTicket] by signing the ticket data
445    /// with the given key. Fails if [signature](TicketBuilder::signature) was previously set.
446    pub fn build_signed(self, signer: &ChainKeypair, domain_separator: &Hash) -> errors::Result<VerifiedTicket> {
447        if self.signature.is_none() {
448            Ok(self.build()?.sign(signer, domain_separator))
449        } else {
450            Err(InvalidInputData("signature already set".into()))
451        }
452    }
453
454    /// Validates all inputs and builds the [VerifiedTicket] by **assuming** the previously
455    /// set [signature](TicketBuilder::signature) is valid and belongs to the given ticket `hash`.
456    /// It does **not** check whether `hash` matches the input data nor that the signature verifies
457    /// the given hash.
458    pub fn build_verified(self, hash: Hash) -> errors::Result<VerifiedTicket> {
459        if let Some(signature) = self.signature {
460            let issuer = signature.recover_from_hash(&hash)?.to_address();
461            Ok(VerifiedTicket(self.build()?, hash, issuer))
462        } else {
463            Err(InvalidInputData("signature is missing".into()))
464        }
465    }
466}
467
468impl From<&Ticket> for TicketBuilder {
469    fn from(value: &Ticket) -> Self {
470        Self {
471            channel_id: Some(value.channel_id),
472            amount: None,
473            balance: Some(value.amount),
474            index: value.index,
475            index_offset: value.index_offset,
476            channel_epoch: value.channel_epoch,
477            win_prob: value.encoded_win_prob.into(),
478            challenge: Some(value.challenge),
479            signature: None,
480        }
481    }
482}
483
484impl From<Ticket> for TicketBuilder {
485    fn from(value: Ticket) -> Self {
486        Self::from(&value)
487    }
488}
489
490#[cfg_attr(doc, aquamarine::aquamarine)]
491/// Contains the overall description of a ticket with a signature.
492///
493/// This structure is not considered [verified](VerifiedTicket), unless
494/// the [Ticket::verify] or [Ticket::sign] methods are called.
495///
496/// # Ticket state machine
497/// See the entire state machine describing the relations of different ticket types below:
498/// ```mermaid
499/// flowchart TB
500///     A[Ticket] -->|verify| B(VerifiedTicket)
501///     B --> |leak| A
502///     A --> |sign| B
503///     B --> |into_unacknowledged| C(UnacknowledgedTicket)
504///     B --> |into_acknowledged| D(AcknowledgedTicket)
505///     C --> |acknowledge| D
506///     D --> |into_redeemable| E(RedeemableTicket)
507///     D --> |into_transferable| F(TransferableWinningTicket)
508///     E --> |into_transferable| F
509///     F --> |into_redeemable| E
510/// ```
511#[derive(Clone, Debug, PartialEq, Eq)]
512#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
513pub struct Ticket {
514    /// Channel ID.
515    /// See [generate_channel_id] for how this value is generated.
516    pub channel_id: Hash,
517    /// Amount of HOPR tokens this ticket is worth.
518    /// Always between 0 and 2^92.
519    pub amount: HoprBalance, // 92 bits
520    /// Ticket index.
521    /// Always between 0 and 2^48.
522    pub index: u64, // 48 bits
523    /// Ticket index offset.
524    /// Always between 1 and 2^32.
525    /// For normal tickets this is always equal to 1, for aggregated this is always > 1.
526    pub index_offset: u32, // 32 bits
527    /// Encoded winning probability represented via 56-bit number.
528    pub encoded_win_prob: EncodedWinProb, // 56 bits
529    /// Epoch of the channel this ticket belongs to.
530    /// Always between 0 and 2^24.
531    pub channel_epoch: u32, // 24 bits
532    /// Represent the Proof of Relay challenge encoded as an Ethereum address.
533    pub challenge: EthereumChallenge,
534    /// ECDSA secp256k1 signature of all the above values.
535    pub signature: Option<Signature>,
536}
537
538impl PartialOrd for Ticket {
539    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
540        Some(self.cmp(other))
541    }
542}
543
544impl Ord for Ticket {
545    fn cmp(&self, other: &Self) -> Ordering {
546        // Ordering:
547        // [channel_id][channel_epoch][ticket_index]
548        match self.channel_id.cmp(&other.channel_id) {
549            Ordering::Equal => match self.channel_epoch.cmp(&other.channel_epoch) {
550                Ordering::Equal => self.index.cmp(&other.index),
551                Ordering::Greater => Ordering::Greater,
552                Ordering::Less => Ordering::Less,
553            },
554            Ordering::Greater => Ordering::Greater,
555            Ordering::Less => Ordering::Less,
556        }
557    }
558}
559
560impl Display for Ticket {
561    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
562        write!(
563            f,
564            "ticket #{}, amount {}, offset {}, epoch {} in channel {}",
565            self.index, self.amount, self.index_offset, self.channel_epoch, self.channel_id
566        )
567    }
568}
569
570impl Ticket {
571    fn encode_without_signature(&self) -> [u8; Self::SIZE - Signature::SIZE] {
572        let mut ret = [0u8; Self::SIZE - Signature::SIZE];
573        let mut offset = 0;
574
575        ret[offset..offset + Hash::SIZE].copy_from_slice(self.channel_id.as_ref());
576        offset += Hash::SIZE;
577
578        // There are only 2^96 HOPR tokens
579        ret[offset..offset + 12].copy_from_slice(&self.amount.amount().to_be_bytes()[20..32]);
580        offset += 12;
581
582        // Ticket index can go only up to 2^48
583        ret[offset..offset + 6].copy_from_slice(&self.index.to_be_bytes()[2..8]);
584        offset += 6;
585
586        ret[offset..offset + 4].copy_from_slice(&self.index_offset.to_be_bytes());
587        offset += 4;
588
589        // Channel epoch can go only up to 2^24
590        ret[offset..offset + 3].copy_from_slice(&self.channel_epoch.to_be_bytes()[1..4]);
591        offset += 3;
592
593        ret[offset..offset + ENCODED_WIN_PROB_LENGTH].copy_from_slice(&self.encoded_win_prob);
594        offset += ENCODED_WIN_PROB_LENGTH;
595
596        ret[offset..offset + EthereumChallenge::SIZE].copy_from_slice(self.challenge.as_ref());
597
598        ret
599    }
600
601    /// Computes Ethereum signature hash of the ticket,
602    /// must be equal to on-chain computation
603    pub fn get_hash(&self, domain_separator: &Hash) -> Hash {
604        let ticket_hash = Hash::create(&[self.encode_without_signature().as_ref()]); // cannot fail
605        let hash_struct = Hash::create(&[&REDEEM_CALL_SELECTOR, &[0u8; 28], ticket_hash.as_ref()]);
606        Hash::create(&[&hex!("1901"), domain_separator.as_ref(), hash_struct.as_ref()])
607    }
608
609    /// Signs the ticket using the given private key, turning this ticket into [VerifiedTicket].
610    /// If a signature was already present, it will be replaced.
611    pub fn sign(mut self, signing_key: &ChainKeypair, domain_separator: &Hash) -> VerifiedTicket {
612        let ticket_hash = self.get_hash(domain_separator);
613        self.signature = Some(Signature::sign_hash(&ticket_hash, signing_key));
614        VerifiedTicket(self, ticket_hash, signing_key.public().to_address())
615    }
616
617    /// Verifies the signature of this ticket, turning this ticket into `VerifiedTicket`.
618    /// If the verification fails, `Self` is returned in the error.
619    ///
620    /// This is done by recovering the signer from the signature and verifying that it matches
621    /// the given `issuer` argument. This is possible due this specific instantiation of the ECDSA
622    /// over the secp256k1 curve.
623    /// The operation can fail if a public key cannot be recovered from the ticket signature.
624    #[instrument(level = "trace", skip_all, err)]
625    pub fn verify(self, issuer: &Address, domain_separator: &Hash) -> Result<VerifiedTicket, Box<Ticket>> {
626        let ticket_hash = self.get_hash(domain_separator);
627
628        if let Some(signature) = &self.signature {
629            match signature.recover_from_hash(&ticket_hash) {
630                Ok(pk) if pk.to_address().eq(issuer) => Ok(VerifiedTicket(self, ticket_hash, *issuer)),
631                Err(e) => {
632                    error!("failed to verify ticket signature: {e}");
633                    Err(self.into())
634                }
635                _ => Err(self.into()),
636            }
637        } else {
638            Err(self.into())
639        }
640    }
641
642    /// Returns true if this ticket aggregates multiple tickets.
643    #[inline]
644    pub fn is_aggregated(&self) -> bool {
645        // Aggregated tickets have always an index offset > 1
646        self.index_offset > 1
647    }
648
649    /// Returns the decoded winning probability of the ticket
650    #[inline]
651    pub fn win_prob(&self) -> WinningProbability {
652        WinningProbability(self.encoded_win_prob)
653    }
654}
655
656impl From<Ticket> for [u8; TICKET_SIZE] {
657    fn from(value: Ticket) -> Self {
658        let mut ret = [0u8; TICKET_SIZE];
659        ret[0..Ticket::SIZE - Signature::SIZE].copy_from_slice(value.encode_without_signature().as_ref());
660        ret[Ticket::SIZE - Signature::SIZE..].copy_from_slice(
661            value
662                .signature
663                .expect("cannot serialize ticket without signature")
664                .as_ref(),
665        );
666        ret
667    }
668}
669
670impl TryFrom<&[u8]> for Ticket {
671    type Error = GeneralError;
672
673    fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
674        if value.len() == Self::SIZE {
675            let mut offset = 0;
676
677            // TODO: not necessary to the ChannelId over the wire, only the counterparty is sufficient
678            let channel_id = Hash::try_from(&value[offset..offset + Hash::SIZE])?;
679            offset += Hash::SIZE;
680
681            let mut amount = [0u8; 32];
682            amount[20..32].copy_from_slice(&value[offset..offset + 12]);
683            offset += 12;
684
685            let mut index = [0u8; 8];
686            index[2..8].copy_from_slice(&value[offset..offset + 6]);
687            offset += 6;
688
689            let mut index_offset = [0u8; 4];
690            index_offset.copy_from_slice(&value[offset..offset + 4]);
691            offset += 4;
692
693            let mut channel_epoch = [0u8; 4];
694            channel_epoch[1..4].copy_from_slice(&value[offset..offset + 3]);
695            offset += 3;
696
697            let win_prob = WinningProbability::try_from(&value[offset..offset + WinningProbability::SIZE])?;
698            offset += WinningProbability::SIZE;
699
700            debug_assert_eq!(offset, ENCODED_TICKET_LENGTH);
701
702            let challenge = EthereumChallenge::try_from(&value[offset..offset + EthereumChallenge::SIZE])?;
703            offset += EthereumChallenge::SIZE;
704
705            let signature = Signature::try_from(&value[offset..offset + Signature::SIZE])?;
706
707            // Validate the boundaries of the parsed values
708            TicketBuilder::default()
709                .channel_id(channel_id)
710                .amount(U256::from_big_endian(&amount))
711                .index(u64::from_be_bytes(index))
712                .index_offset(u32::from_be_bytes(index_offset))
713                .channel_epoch(u32::from_be_bytes(channel_epoch))
714                .win_prob(win_prob)
715                .eth_challenge(challenge)
716                .signature(signature)
717                .build()
718                .map_err(|e| GeneralError::ParseError(format!("ticket build failed: {e}")))
719        } else {
720            Err(GeneralError::ParseError("Ticket".into()))
721        }
722    }
723}
724
725const TICKET_SIZE: usize = ENCODED_TICKET_LENGTH + EthereumChallenge::SIZE + Signature::SIZE;
726
727impl BytesEncodable<TICKET_SIZE> for Ticket {}
728
729/// Holds a ticket that has been already verified.
730/// This structure guarantees that [`Ticket::get_hash()`] of [`VerifiedTicket::verified_ticket()`]
731/// is always equal to [`VerifiedTicket::verified_hash`]
732#[derive(Debug, Clone, PartialEq, Eq)]
733#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
734pub struct VerifiedTicket(Ticket, Hash, Address);
735
736impl VerifiedTicket {
737    /// Returns the verified encoded winning probability of the ticket
738    #[inline]
739    pub fn win_prob(&self) -> WinningProbability {
740        self.0.win_prob()
741    }
742
743    /// Checks if this ticket is considered a win.
744    /// Requires access to the private key to compute the VRF values.
745    ///
746    /// Computes the ticket's luck value and compares it against the
747    /// ticket's probability. If luck <= probability, the ticket is
748    /// considered a win.
749    ///
750    /// ## Ticket luck value
751    /// This ticket's `luck value` is the first 7 bytes of Keccak256 hash
752    /// of the concatenation of ticket's hash, VRF's encoded `v` value,
753    /// PoR response and the ticket's signature.
754    ///
755    /// ## Winning probability
756    /// Each ticket specifies a probability, given as an integer in
757    /// [0, 2^56 - 1] where 0 -> 0% and 2^56 - 1 -> 100% win
758    /// probability. If the ticket's luck value is greater than
759    /// the stated probability, it is considered a winning ticket.
760    pub fn is_winning(&self, response: &Response, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
761        if let Ok(vrf_params) = derive_vrf_parameters(self.1, chain_keypair, domain_separator.as_ref()) {
762            check_ticket_win(
763                &self.1,
764                self.0
765                    .signature
766                    .as_ref()
767                    .expect("verified ticket have always a signature"),
768                &self.0.win_prob(),
769                response,
770                &vrf_params,
771            )
772        } else {
773            error!("cannot derive vrf parameters for {self}");
774            false
775        }
776    }
777
778    /// Ticket with already verified signature.
779    #[inline]
780    pub fn verified_ticket(&self) -> &Ticket {
781        &self.0
782    }
783
784    /// Fixed ticket hash that is guaranteed to be equal to
785    /// [`Ticket::get_hash`] of [`VerifiedTicket::verified_ticket`].
786    #[inline]
787    pub fn verified_hash(&self) -> &Hash {
788        &self.1
789    }
790
791    /// Verified issuer of the ticket.
792    /// The returned address is guaranteed to be equal to the signer
793    /// recovered from the [`VerifiedTicket::verified_ticket`]'s signature.
794    #[inline]
795    pub fn verified_issuer(&self) -> &Address {
796        &self.2
797    }
798
799    /// Shorthand to retrieve reference to the verified ticket signature
800    pub fn verified_signature(&self) -> &Signature {
801        self.0
802            .signature
803            .as_ref()
804            .expect("verified ticket always has a signature")
805    }
806
807    /// Deconstructs self back into the unverified [Ticket].
808    #[inline]
809    pub fn leak(self) -> Ticket {
810        self.0
811    }
812
813    /// Creates a new unacknowledged ticket from the [VerifiedTicket],
814    /// given our own part of the PoR challenge.
815    pub fn into_unacknowledged(self, own_key: HalfKey) -> UnacknowledgedTicket {
816        UnacknowledgedTicket { ticket: self, own_key }
817    }
818
819    /// Shorthand to acknowledge the ticket if the matching response is already known.
820    /// This is used upon receiving an aggregated ticket.
821    pub fn into_acknowledged(self, response: Response) -> AcknowledgedTicket {
822        AcknowledgedTicket {
823            status: AcknowledgedTicketStatus::Untouched,
824            ticket: self,
825            response,
826        }
827    }
828}
829
830impl Display for VerifiedTicket {
831    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
832        write!(f, "verified {}", self.0)
833    }
834}
835
836impl PartialOrd for VerifiedTicket {
837    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
838        Some(self.cmp(other))
839    }
840}
841
842impl Ord for VerifiedTicket {
843    fn cmp(&self, other: &Self) -> Ordering {
844        self.0.cmp(&other.0)
845    }
846}
847
848/// Represents a [VerifiedTicket] with an unknown other part of the [HalfKey].
849/// Once the other [HalfKey] is known (forming a [Response]),
850/// it can be [acknowledged](UnacknowledgedTicket::acknowledge).
851#[derive(Clone, Debug, PartialEq, Eq)]
852#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
853pub struct UnacknowledgedTicket {
854    pub ticket: VerifiedTicket,
855    pub(crate) own_key: HalfKey,
856}
857
858impl UnacknowledgedTicket {
859    /// Convenience method to retrieve a reference to the underlying verified [Ticket].
860    #[inline]
861    pub fn verified_ticket(&self) -> &Ticket {
862        self.ticket.verified_ticket()
863    }
864
865    /// Verifies that the given acknowledgement solves this ticket's challenge and then
866    /// turns this unacknowledged ticket into an acknowledged ticket by adding
867    /// the received acknowledgement of the forwarded packet.
868    pub fn acknowledge(self, acknowledgement: &HalfKey) -> crate::errors::Result<AcknowledgedTicket> {
869        let response = Response::from_half_keys(&self.own_key, acknowledgement)?;
870        debug!(ticket = %self.ticket, response = response.to_hex(), "acknowledging ticket using response");
871
872        if self.ticket.verified_ticket().challenge == response.to_challenge()?.to_ethereum_challenge() {
873            Ok(self.ticket.into_acknowledged(response))
874        } else {
875            Err(CryptoError::InvalidChallenge.into())
876        }
877    }
878}
879
880/// Status of the acknowledged ticket.
881#[repr(u8)]
882#[derive(
883    Clone,
884    Copy,
885    Debug,
886    Default,
887    Eq,
888    PartialEq,
889    strum::Display,
890    strum::EnumString,
891    num_enum::IntoPrimitive,
892    num_enum::TryFromPrimitive,
893)]
894#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
895#[strum(serialize_all = "PascalCase")]
896pub enum AcknowledgedTicketStatus {
897    /// The ticket is available for redeeming or aggregating
898    #[default]
899    Untouched = 0,
900    /// Ticket is currently being redeemed in and ongoing redemption process
901    BeingRedeemed = 1,
902    /// Ticket is currently being aggregated in and ongoing aggregation process
903    BeingAggregated = 2,
904}
905
906/// Contains acknowledgment information and the respective ticket
907#[derive(Clone, Debug, PartialEq, Eq)]
908#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
909pub struct AcknowledgedTicket {
910    #[cfg_attr(feature = "serde", serde(default))]
911    pub status: AcknowledgedTicketStatus,
912    pub ticket: VerifiedTicket,
913    pub response: Response,
914}
915
916impl PartialOrd for AcknowledgedTicket {
917    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
918        Some(self.cmp(other))
919    }
920}
921
922impl Ord for AcknowledgedTicket {
923    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
924        self.ticket.cmp(&other.ticket)
925    }
926}
927
928impl AcknowledgedTicket {
929    /// Convenience method to retrieve a reference to the underlying verified [Ticket].
930    #[inline]
931    pub fn verified_ticket(&self) -> &Ticket {
932        self.ticket.verified_ticket()
933    }
934
935    /// Checks if this acknowledged ticket is winning.
936    pub fn is_winning(&self, chain_keypair: &ChainKeypair, domain_separator: &Hash) -> bool {
937        self.ticket.is_winning(&self.response, chain_keypair, domain_separator)
938    }
939
940    /// Transforms this ticket into [`RedeemableTicket`] that can be redeemed on-chain
941    /// or transformed into [`TransferableWinningTicket`] that can be sent for aggregation.
942    ///
943    /// The `chain_keypair` must not be of the ticket's issuer.
944    /// This ticket MUST be winning, otherwise the function fails with [`CoreTypesError::TicketNotWinning`].
945    pub fn into_redeemable(
946        self,
947        chain_keypair: &ChainKeypair,
948        domain_separator: &Hash,
949    ) -> crate::errors::Result<RedeemableTicket> {
950        // This function must be called by the ticket recipient and not the issuer
951        if chain_keypair.public().to_address().eq(self.ticket.verified_issuer()) {
952            return Err(errors::CoreTypesError::LoopbackTicket);
953        }
954
955        let vrf_params = derive_vrf_parameters(self.ticket.verified_hash(), chain_keypair, domain_separator.as_ref())?;
956
957        if !check_ticket_win(
958            self.ticket.verified_hash(),
959            self.ticket.verified_signature(),
960            &self.ticket.win_prob(),
961            &self.response,
962            &vrf_params,
963        ) {
964            return Err(CoreTypesError::TicketNotWinning);
965        }
966
967        Ok(RedeemableTicket {
968            ticket: self.ticket,
969            response: self.response,
970            vrf_params,
971            channel_dst: *domain_separator,
972        })
973    }
974
975    /// Shorthand for transforming this ticket into [TransferableWinningTicket].
976    /// See [`AcknowledgedTicket::into_redeemable`] for details.
977    pub fn into_transferable(
978        self,
979        chain_keypair: &ChainKeypair,
980        domain_separator: &Hash,
981    ) -> errors::Result<TransferableWinningTicket> {
982        self.into_redeemable(chain_keypair, domain_separator)
983            .map(TransferableWinningTicket::from)
984    }
985}
986
987impl Display for AcknowledgedTicket {
988    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
989        write!(f, "acknowledged {} in state '{}'", self.ticket, self.status)
990    }
991}
992
993/// Represents a winning ticket that can be successfully redeemed on-chain.
994#[derive(Clone, Debug)]
995#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
996pub struct RedeemableTicket {
997    /// Verified ticket that can be redeemed.
998    pub ticket: VerifiedTicket,
999    /// Solution to the PoR challenge in the ticket.
1000    pub response: Response,
1001    /// VRF parameters required for redeeming.
1002    pub vrf_params: VrfParameters,
1003    /// Channel domain separator used to compute the VRF parameters.
1004    pub channel_dst: Hash,
1005}
1006
1007impl RedeemableTicket {
1008    /// Convenience method to retrieve a reference to the underlying verified [Ticket].
1009    #[inline]
1010    pub fn verified_ticket(&self) -> &Ticket {
1011        self.ticket.verified_ticket()
1012    }
1013}
1014
1015impl PartialEq for RedeemableTicket {
1016    fn eq(&self, other: &Self) -> bool {
1017        self.ticket == other.ticket && self.channel_dst == other.channel_dst && self.response == other.response
1018    }
1019}
1020
1021impl Display for RedeemableTicket {
1022    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1023        write!(f, "redeemable {}", self.ticket)
1024    }
1025}
1026
1027impl From<RedeemableTicket> for AcknowledgedTicket {
1028    fn from(value: RedeemableTicket) -> Self {
1029        Self {
1030            status: AcknowledgedTicketStatus::Untouched,
1031            ticket: value.ticket,
1032            response: value.response,
1033        }
1034    }
1035}
1036
1037/// Represents a ticket that could be transferred over the wire
1038/// and independently verified again by the other party.
1039///
1040/// The [TransferableWinningTicket] can be easily retrieved from [RedeemableTicket], which strips
1041/// information about verification.
1042/// [TransferableWinningTicket] can be attempted to be converted back to [RedeemableTicket] only
1043/// when verified via [`TransferableWinningTicket::into_redeemable`] again.
1044#[derive(Debug, Clone)]
1045#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1046pub struct TransferableWinningTicket {
1047    pub ticket: Ticket,
1048    pub response: Response,
1049    pub vrf_params: VrfParameters,
1050    pub signer: Address,
1051}
1052
1053impl TransferableWinningTicket {
1054    /// Attempts to transform this ticket back into a [RedeemableTicket].
1055    ///
1056    /// Verifies that the `signer` matches the `expected_issuer` and that the
1057    /// ticket has a valid signature from the `signer`.
1058    /// Then it verifies if the ticket is winning and therefore if it can be successfully
1059    /// redeemed on-chain.
1060    pub fn into_redeemable(
1061        self,
1062        expected_issuer: &Address,
1063        domain_separator: &Hash,
1064    ) -> errors::Result<RedeemableTicket> {
1065        if !self.signer.eq(expected_issuer) {
1066            return Err(crate::errors::CoreTypesError::InvalidInputData(
1067                "invalid ticket issuer".into(),
1068            ));
1069        }
1070
1071        let verified_ticket = self
1072            .ticket
1073            .verify(&self.signer, domain_separator)
1074            .map_err(|_| CoreTypesError::CryptoError(CryptoError::SignatureVerification))?;
1075
1076        if check_ticket_win(
1077            verified_ticket.verified_hash(),
1078            verified_ticket.verified_signature(),
1079            &verified_ticket.verified_ticket().win_prob(),
1080            &self.response,
1081            &self.vrf_params,
1082        ) {
1083            Ok(RedeemableTicket {
1084                ticket: verified_ticket,
1085                response: self.response,
1086                vrf_params: self.vrf_params,
1087                channel_dst: *domain_separator,
1088            })
1089        } else {
1090            Err(crate::errors::CoreTypesError::InvalidInputData(
1091                "ticket is not a win".into(),
1092            ))
1093        }
1094    }
1095}
1096
1097impl PartialEq for TransferableWinningTicket {
1098    fn eq(&self, other: &Self) -> bool {
1099        self.ticket == other.ticket && self.signer == other.signer && self.response == other.response
1100    }
1101}
1102
1103impl PartialOrd<Self> for TransferableWinningTicket {
1104    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1105        Some(self.ticket.cmp(&other.ticket))
1106    }
1107}
1108
1109impl From<RedeemableTicket> for TransferableWinningTicket {
1110    fn from(value: RedeemableTicket) -> Self {
1111        Self {
1112            response: value.response,
1113            vrf_params: value.vrf_params,
1114            signer: *value.ticket.verified_issuer(),
1115            ticket: value.ticket.leak(),
1116        }
1117    }
1118}
1119
1120#[cfg(test)]
1121pub mod tests {
1122    use hex_literal::hex;
1123    use hopr_crypto_random::Randomizable;
1124    use hopr_crypto_types::{
1125        keypairs::{ChainKeypair, Keypair},
1126        types::{HalfKey, Hash, Response},
1127    };
1128    use hopr_primitive_types::{
1129        prelude::UnitaryFloatOps,
1130        primitives::{Address, EthereumChallenge, U256},
1131    };
1132
1133    use super::*;
1134
1135    lazy_static::lazy_static! {
1136        static ref ALICE: ChainKeypair = ChainKeypair::from_secret(&hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775")).expect("lazy static keypair should be constructible");
1137        static ref BOB: ChainKeypair = ChainKeypair::from_secret(&hex!("48680484c6fc31bc881a0083e6e32b6dc789f9eaba0f8b981429fd346c697f8c")).expect("lazy static keypair should be constructible");
1138    }
1139
1140    #[cfg(feature = "serde")]
1141    const BINCODE_CONFIGURATION: bincode::config::Configuration = bincode::config::standard()
1142        .with_little_endian()
1143        .with_variable_int_encoding();
1144
1145    #[test]
1146    pub fn test_win_prob_to_f64() -> anyhow::Result<()> {
1147        assert_eq!(0.0f64, WinningProbability::NEVER.as_f64());
1148
1149        assert_eq!(1.0f64, WinningProbability::ALWAYS.as_f64());
1150
1151        let mut test_bit_string = [0xffu8; 7];
1152        test_bit_string[0] = 0x7f;
1153        assert_eq!(0.5f64, WinningProbability::from(&test_bit_string).as_f64());
1154
1155        test_bit_string[0] = 0x3f;
1156        assert_eq!(0.25f64, WinningProbability::from(&test_bit_string).as_f64());
1157
1158        test_bit_string[0] = 0x1f;
1159        assert_eq!(0.125f64, WinningProbability::from(&test_bit_string).as_f64());
1160
1161        Ok(())
1162    }
1163
1164    #[test]
1165    pub fn test_f64_to_win_prob() -> anyhow::Result<()> {
1166        assert_eq!([0u8; 7], WinningProbability::try_from(0.0f64)?.as_encoded());
1167
1168        let mut test_bit_string = [0xffu8; 7];
1169        assert_eq!(test_bit_string, WinningProbability::try_from(1.0f64)?.as_encoded());
1170
1171        test_bit_string[0] = 0x7f;
1172        assert_eq!(test_bit_string, WinningProbability::try_from(0.5f64)?.as_encoded());
1173
1174        test_bit_string[0] = 0x3f;
1175        assert_eq!(test_bit_string, WinningProbability::try_from(0.25f64)?.as_encoded());
1176
1177        test_bit_string[0] = 0x1f;
1178        assert_eq!(test_bit_string, WinningProbability::try_from(0.125f64)?.as_encoded());
1179
1180        Ok(())
1181    }
1182
1183    #[test]
1184    pub fn test_win_prob_approx_eq() -> anyhow::Result<()> {
1185        let wp_0 = WinningProbability(hex!("0020C49BBFFFFF"));
1186        let wp_1 = WinningProbability(hex!("0020C49BA5E34F"));
1187
1188        assert_ne!(wp_0.as_ref(), wp_1.as_ref());
1189        assert_eq!(wp_0, wp_1.as_f64());
1190
1191        Ok(())
1192    }
1193
1194    #[test]
1195    pub fn test_win_prob_back_and_forth() -> anyhow::Result<()> {
1196        for float in [0.1f64, 0.002f64, 0.00001f64, 0.7311111f64, 1.0f64, 0.0f64] {
1197            assert!((float - WinningProbability::try_from_f64(float)?.as_f64()).abs() < f64::EPSILON);
1198        }
1199
1200        Ok(())
1201    }
1202
1203    #[test]
1204    pub fn test_win_prob_must_be_correctly_ordered() {
1205        let increment = WinningProbability::EPSILON * 100.0; // Testing the entire range would take too long
1206        let mut prev = WinningProbability::NEVER;
1207        while let Ok(next) = WinningProbability::try_from_f64(prev.as_f64() + increment) {
1208            assert!(prev.approx_cmp(&next).is_lt());
1209            prev = next;
1210        }
1211    }
1212
1213    #[test]
1214    pub fn test_win_prob_epsilon_must_be_never() -> anyhow::Result<()> {
1215        assert!(WinningProbability::NEVER.approx_eq(&WinningProbability::try_from_f64(WinningProbability::EPSILON)?));
1216        Ok(())
1217    }
1218
1219    #[test]
1220    pub fn test_win_prob_bounds_must_be_eq() -> anyhow::Result<()> {
1221        let bound = 0.1 + WinningProbability::EPSILON;
1222        let other = 0.1;
1223        assert!(WinningProbability::try_from_f64(bound)?.approx_eq(&WinningProbability::try_from_f64(other)?));
1224        Ok(())
1225    }
1226
1227    #[test]
1228    pub fn test_ticket_builder_zero_hop() -> anyhow::Result<()> {
1229        let ticket = TicketBuilder::zero_hop()
1230            .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1231            .eth_challenge(Default::default())
1232            .build()?;
1233        assert_eq!(0, ticket.index);
1234        assert_eq!(0.0, ticket.win_prob().as_f64());
1235        assert_eq!(0, ticket.channel_epoch);
1236        assert_eq!(
1237            generate_channel_id(&ALICE.public().to_address(), &BOB.public().to_address()),
1238            ticket.channel_id
1239        );
1240        Ok(())
1241    }
1242
1243    #[test]
1244    pub fn test_ticket_serialize_deserialize() -> anyhow::Result<()> {
1245        let initial_ticket = TicketBuilder::default()
1246            .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1247            .balance(1.into())
1248            .index(0)
1249            .index_offset(1)
1250            .win_prob(1.0.try_into()?)
1251            .channel_epoch(1)
1252            .eth_challenge(Default::default())
1253            .build_signed(&ALICE, &Default::default())?;
1254
1255        assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
1256
1257        let ticket_bytes: [u8; Ticket::SIZE] = initial_ticket.verified_ticket().clone().into();
1258        assert_eq!(
1259            initial_ticket.verified_ticket(),
1260            &Ticket::try_from(ticket_bytes.as_ref())?
1261        );
1262        Ok(())
1263    }
1264
1265    #[test]
1266    #[cfg(feature = "serde")]
1267    pub fn test_ticket_serialize_deserialize_serde() -> anyhow::Result<()> {
1268        let initial_ticket = TicketBuilder::default()
1269            .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1270            .balance(1.into())
1271            .index(0)
1272            .index_offset(1)
1273            .win_prob(1.0.try_into()?)
1274            .channel_epoch(1)
1275            .eth_challenge(Default::default())
1276            .build_signed(&ALICE, &Default::default())?;
1277
1278        assert_eq!(
1279            initial_ticket,
1280            bincode::serde::decode_from_slice(
1281                &bincode::serde::encode_to_vec(&initial_ticket, BINCODE_CONFIGURATION)?,
1282                BINCODE_CONFIGURATION
1283            )
1284            .map(|v| v.0)?
1285        );
1286        Ok(())
1287    }
1288
1289    #[test]
1290    pub fn test_ticket_sign_verify() -> anyhow::Result<()> {
1291        let initial_ticket = TicketBuilder::default()
1292            .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1293            .balance(1.into())
1294            .index(0)
1295            .index_offset(1)
1296            .win_prob(1.0.try_into()?)
1297            .channel_epoch(1)
1298            .eth_challenge(Default::default())
1299            .build_signed(&ALICE, &Default::default())?;
1300
1301        assert_ne!(initial_ticket.verified_hash().as_ref(), [0u8; Hash::SIZE]);
1302
1303        let ticket = initial_ticket.leak();
1304        assert!(ticket.verify(&ALICE.public().to_address(), &Default::default()).is_ok());
1305        Ok(())
1306    }
1307
1308    #[test]
1309    pub fn test_zero_hop() -> anyhow::Result<()> {
1310        let ticket = TicketBuilder::zero_hop()
1311            .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1312            .eth_challenge(Default::default())
1313            .build_signed(&ALICE, &Default::default())?;
1314
1315        assert!(
1316            ticket
1317                .leak()
1318                .verify(&ALICE.public().to_address(), &Hash::default())
1319                .is_ok()
1320        );
1321        Ok(())
1322    }
1323
1324    fn mock_ticket(
1325        pk: &ChainKeypair,
1326        counterparty: &Address,
1327        domain_separator: Option<Hash>,
1328        challenge: Option<EthereumChallenge>,
1329    ) -> anyhow::Result<VerifiedTicket> {
1330        let win_prob = 1.0f64; // 100 %
1331        let price_per_packet: U256 = 10000000000000000u128.into(); // 0.01 HOPR
1332        let path_pos = 5u64;
1333
1334        Ok(TicketBuilder::default()
1335            .direction(&pk.public().to_address(), counterparty)
1336            .amount(price_per_packet.div_f64(win_prob)? * U256::from(path_pos))
1337            .index(0)
1338            .index_offset(1)
1339            .win_prob(1.0.try_into()?)
1340            .channel_epoch(4)
1341            .eth_challenge(challenge.unwrap_or_default())
1342            .build_signed(pk, &domain_separator.unwrap_or_default())?)
1343    }
1344
1345    #[test]
1346    fn test_unacknowledged_ticket_challenge_response() -> anyhow::Result<()> {
1347        let hk1 = HalfKey::try_from(hex!("3477d7de923ba3a7d5d72a7d6c43fd78395453532d03b2a1e2b9a7cc9b61bafa").as_ref())?;
1348
1349        let hk2 = HalfKey::try_from(hex!("4471496ef88d9a7d86a92b7676f3c8871a60792a37fae6fc3abc347c3aa3b16b").as_ref())?;
1350
1351        let challenge = Response::from_half_keys(&hk1, &hk2)?.to_challenge()?;
1352
1353        let dst = Hash::default();
1354        let ack = mock_ticket(
1355            &ALICE,
1356            &BOB.public().to_address(),
1357            Some(dst),
1358            Some(challenge.to_ethereum_challenge()),
1359        )?
1360        .into_unacknowledged(hk1)
1361        .acknowledge(&hk2)?;
1362
1363        assert!(ack.is_winning(&BOB, &dst), "ticket must be winning");
1364        Ok(())
1365    }
1366
1367    #[test]
1368    #[cfg(feature = "serde")]
1369    fn test_acknowledged_ticket_serde() -> anyhow::Result<()> {
1370        let response =
1371            Response::try_from(hex!("876a41ee5fb2d27ac14d8e8d552692149627c2f52330ba066f9e549aef762f73").as_ref())?;
1372
1373        let dst = Hash::default();
1374
1375        let ticket = mock_ticket(
1376            &ALICE,
1377            &BOB.public().to_address(),
1378            Some(dst),
1379            Some(response.to_challenge()?.to_ethereum_challenge()),
1380        )?;
1381
1382        let acked_ticket = ticket.into_acknowledged(response);
1383
1384        let mut deserialized_ticket = bincode::serde::decode_from_slice(
1385            &bincode::serde::encode_to_vec(&acked_ticket, BINCODE_CONFIGURATION)?,
1386            BINCODE_CONFIGURATION,
1387        )
1388        .map(|v| v.0)?;
1389        assert_eq!(acked_ticket, deserialized_ticket);
1390
1391        assert!(deserialized_ticket.is_winning(&BOB, &dst));
1392
1393        deserialized_ticket.status = super::AcknowledgedTicketStatus::BeingAggregated;
1394
1395        assert_eq!(
1396            deserialized_ticket,
1397            bincode::serde::decode_from_slice(
1398                &bincode::serde::encode_to_vec(&deserialized_ticket, BINCODE_CONFIGURATION)?,
1399                BINCODE_CONFIGURATION,
1400            )
1401            .map(|v| v.0)?
1402        );
1403        Ok(())
1404    }
1405
1406    #[test]
1407    fn test_ticket_entire_ticket_transfer_flow() -> anyhow::Result<()> {
1408        let hk1 = HalfKey::random();
1409        let hk2 = HalfKey::random();
1410        let resp = Response::from_half_keys(&hk1, &hk2)?;
1411
1412        let verified = TicketBuilder::default()
1413            .direction(&ALICE.public().to_address(), &BOB.public().to_address())
1414            .balance(1.into())
1415            .index(0)
1416            .index_offset(1)
1417            .win_prob(1.0.try_into()?)
1418            .channel_epoch(1)
1419            .challenge(resp.to_challenge()?)
1420            .build_signed(&ALICE, &Default::default())?;
1421
1422        let unack = verified.into_unacknowledged(hk1);
1423        let acknowledged = unack.acknowledge(&hk2).expect("should acknowledge");
1424
1425        let redeemable_1 = acknowledged.clone().into_redeemable(&BOB, &Hash::default())?;
1426
1427        let transferable = acknowledged.into_transferable(&BOB, &Hash::default())?;
1428
1429        let redeemable_2 = transferable.into_redeemable(&ALICE.public().to_address(), &Hash::default())?;
1430
1431        assert_eq!(redeemable_1, redeemable_2);
1432        assert_eq!(redeemable_1.vrf_params.V, redeemable_2.vrf_params.V);
1433        Ok(())
1434    }
1435}