hopr_internal_types/
tickets.rs

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
16/// Size-optimized encoding of the ticket, used for both,
17/// network transfer and in the smart contract.
18const ENCODED_TICKET_LENGTH: usize = 64;
19
20/// Custom float to integer encoding used in the integer-only
21/// Ethereum Virtual Machine (EVM). Chosen to be easily
22/// convertible to IEEE754 double-precision and vice versa
23const ENCODED_WIN_PROB_LENGTH: usize = 7;
24
25/// Winning probability encoded in 7-byte representation
26pub type EncodedWinProb = [u8; ENCODED_WIN_PROB_LENGTH];
27
28/// Encodes 100% winning probability
29pub const ALWAYS_WINNING: EncodedWinProb = hex!("ffffffffffffff");
30
31/// Encodes 0% winning probability
32pub const NEVER_WINNING: EncodedWinProb = hex!("00000000000000");
33
34/// Helper function checks if the given ticket values belong to a winning ticket.
35pub(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    // Signed winning probability
43    let mut signed_ticket_luck = [0u8; 8];
44    signed_ticket_luck[1..].copy_from_slice(win_prob);
45
46    // Computed winning probability
47    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..], // skip prefix
52            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/// Builder for [Ticket] and [VerifiedTicket].
62///
63/// A new builder is created via [TicketBuilder::default] or [TicketBuilder::zero_hop].
64///
65/// Input validation is performed upon calling [TicketBuilder::build], [TicketBuilder::build_signed]
66/// and [TicketBuilder::build_verified].
67#[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    /// Initializes the builder for a zero-hop ticket.
87    #[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    /// Sets channel id based on the `source` and `destination`.
100    /// This, [TicketBuilder::channel_id] or [TicketBuilder::addresses] must be set.
101    #[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    /// Sets channel id based on the `source` and `destination`.
108    /// This, [TicketBuilder::channel_id] or [TicketBuilder::direction] must be set.
109    #[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    /// Sets the channel id.
116    /// This, [TicketBuilder::addresses] or [TicketBuilder::direction] must be set.
117    #[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    /// Sets the ticket amount.
124    /// This or [TicketBuilder::balance] must be set and be less or equal to 10^25.
125    #[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    /// Sets the ticket amount as HOPR balance.
133    /// This or [TicketBuilder::amount] must be set and be less or equal to 10^25.
134    #[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    /// Sets the ticket index.
142    /// Must be less or equal to 2^48.
143    /// Defaults to 0.
144    #[must_use]
145    pub fn index(mut self, index: u64) -> Self {
146        self.index = index;
147        self
148    }
149
150    /// Sets the index offset.
151    /// Must be greater or equal 1.
152    /// Defaults to 1.
153    #[must_use]
154    pub fn index_offset(mut self, index_offset: u32) -> Self {
155        self.index_offset = index_offset;
156        self
157    }
158
159    /// Sets the channel epoch.
160    /// Must be less or equal to 2^24.
161    /// Defaults to 1.
162    #[must_use]
163    pub fn channel_epoch(mut self, channel_epoch: u32) -> Self {
164        self.channel_epoch = channel_epoch;
165        self
166    }
167
168    /// Sets the ticket winning probability.
169    /// Mutually exclusive with [TicketBuilder::win_prob_encoded].
170    /// Defaults to 1.0
171    #[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    /// Sets the encoded ticket winning probability.
179    /// Mutually exclusive with [TicketBuilder::win_prob].
180    /// Defaults to [ALWAYS_WINNING].
181    #[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    /// Sets the [EthereumChallenge] for Proof of Relay.
189    /// Must be set.
190    #[must_use]
191    pub fn challenge(mut self, challenge: EthereumChallenge) -> Self {
192        self.challenge = Some(challenge);
193        self
194    }
195
196    /// Set the signature of this ticket.
197    /// Defaults to `None`.
198    #[must_use]
199    pub fn signature(mut self, signature: Signature) -> Self {
200        self.signature = Some(signature);
201        self
202    }
203
204    /// Verifies all inputs and builds the [Ticket].
205    /// This **does not** perform signature verification if a [signature](TicketBuilder::signature)
206    /// was set.
207    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    /// Validates all inputs and builds the [VerifiedTicket] by signing the ticket data
264    /// with the given key. Fails if [signature](TicketBuilder::signature) was previously set.
265    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    /// Validates all input and builds the [VerifiedTicket] by **assuming** the previously
274    /// set [signature](TicketBuilder::signature) is valid and belongs to the given ticket `hash`.
275    /// It does **not** check whether `hash` matches the input data nor that the signature verifies
276    /// the given hash.
277    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/// Contains the overall description of a ticket with a signature.
312///
313/// This structure is not considered [verified](VerifiedTicket), unless
314/// the [Ticket::verify] or [Ticket::sign] methods are called.
315///
316/// # Ticket state machine
317/// See the entire state machine describing the relations of different ticket types below:
318///```mermaid
319///flowchart TB
320///     A[Ticket] -->|verify| B(VerifiedTicket)
321///     B --> |leak| A
322///     A --> |sign| B
323///     B --> |into_unacknowledged| C(UnacknowledgedTicket)
324///     B --> |into_acknowledged| D(AcknowledgedTicket)
325///     C --> |acknowledge| D
326///     D --> |into_redeemable| E(RedeemableTicket)
327///     D --> |into_transferable| F(TransferableWinningTicket)
328///     E --> |into_transferable| F
329///     F --> |into_redeemable| E
330///```
331#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
332pub struct Ticket {
333    /// Channel ID.
334    /// See [generate_channel_id] for how this value is generated.
335    pub channel_id: Hash,
336    /// Amount of HOPR tokens this ticket is worth.
337    /// Always between 0 and 2^92.
338    pub amount: Balance, // 92 bits
339    /// Ticket index.
340    /// Always between 0 and 2^48.
341    pub index: u64, // 48 bits
342    /// Ticket index offset.
343    /// Always between 1 and 2^32.
344    /// For normal tickets this is always equal to 1, for aggregated this is always > 1.
345    pub index_offset: u32, // 32 bits
346    /// Encoded winning probability represented via 56-bit number.
347    pub encoded_win_prob: EncodedWinProb, // 56 bits
348    /// Epoch of the channel this ticket belongs to.
349    /// Always between 0 and 2^24.
350    pub channel_epoch: u32, // 24 bits
351    /// Represent the Proof of Relay challenge encoded as Ethereum address.
352    pub challenge: EthereumChallenge,
353    /// ECDSA secp256k1 signature of all the above values.
354    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        // Ordering:
366        // [channel_id][channel_epoch][ticket_index]
367        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        // There are only 2^96 HOPR tokens
398        ret[offset..offset + 12].copy_from_slice(&self.amount.amount().to_be_bytes()[20..32]);
399        offset += 12;
400
401        // Ticket index can go only up to 2^48
402        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        // Channel epoch can go only up to 2^24
409        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    /// Computes Ethereum signature hash of the ticket,
421    /// must be equal to on-chain computation
422    pub fn get_hash(&self, domain_separator: &Hash) -> Hash {
423        let ticket_hash = Hash::create(&[self.encode_without_signature().as_ref()]); // cannot fail
424        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    /// Signs the ticket using the given private key, turning this ticket into [VerifiedTicket].
429    /// If a signature was already present, it will be replaced.
430    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    /// Verifies the signature of this ticket, turning this ticket into `VerifiedTicket`.
437    /// If the verification fails, `Self` is returned in the error.
438    ///
439    /// This is done by recovering the signer from the signature and verifying that it matches
440    /// the given `issuer` argument. This is possible due this specific instantiation of the ECDSA
441    /// over the secp256k1 curve.
442    /// The operation can fail if a public key cannot be recovered from the ticket signature.
443    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    /// Returns true if this ticket aggregates multiple tickets.
461    pub fn is_aggregated(&self) -> bool {
462        // Aggregated tickets have always an index offset > 1
463        self.index_offset > 1
464    }
465
466    /// Returns the decoded winning probability of the ticket
467    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            // TODO: not necessary to the ChannelId over the wire, only the counterparty is sufficient
494            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            // Validate the boundaries of the parsed values
525            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/// Holds a ticket that has been already verified.
547/// This structure guarantees that [`Ticket::get_hash()`] of [`VerifiedTicket::verified_ticket()`]
548/// is always equal to [`VerifiedTicket::verified_hash`]
549#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
550pub struct VerifiedTicket(Ticket, Hash, Address);
551
552impl VerifiedTicket {
553    /// Returns the verified encoded winning probability of the ticket
554    pub fn win_prob(&self) -> f64 {
555        self.0.win_prob()
556    }
557
558    /// Checks if this ticket is considered a win.
559    /// Requires access to the private key to compute the VRF values.
560    ///
561    /// Computes the ticket's luck value and compares it against the
562    /// ticket's probability. If luck <= probability, the ticket is
563    /// considered a win.
564    ///
565    /// ## Ticket luck value
566    /// This ticket's `luck value` is the first 7 bytes of Keccak256 hash
567    /// of the concatenation of ticket's hash, VRF's encoded `v` value,
568    /// PoR response and the ticket's signature.
569    ///
570    /// ## Winning probability
571    /// Each ticket specifies a probability, given as an integer in
572    /// [0, 2^56 - 1] where 0 -> 0% and 2^56 - 1 -> 100% win
573    /// probability. If the ticket's luck value is greater than
574    /// the stated probability, it is considered a winning ticket.
575    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    /// Based on the price of this ticket, determines the path position (hop number) this ticket
594    /// relates to.
595    ///
596    /// This is done by first determining the amount of tokens the ticket is worth
597    /// if it were a win and redeemed on-chain.
598    ///
599    /// Does not support path lengths greater than 255.
600    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() // convert to u8 = makes sure it's < 256
604            .map_err(|_| CoreTypesError::ArithmeticError(format!("Cannot convert {pos} to u8")))
605    }
606
607    /// Ticket with already verified signature.
608    pub fn verified_ticket(&self) -> &Ticket {
609        &self.0
610    }
611
612    /// Fixed ticket hash that is guaranteed to be equal to
613    /// [`Ticket::get_hash`] of [`VerifiedTicket::verified_ticket`].
614    pub fn verified_hash(&self) -> &Hash {
615        &self.1
616    }
617
618    /// Verified issuer of the ticket.
619    /// The returned address is guaranteed to be equal to the signer
620    /// recovered from the [`VerifiedTicket::verified_ticket`]'s signature.
621    pub fn verified_issuer(&self) -> &Address {
622        &self.2
623    }
624
625    /// Shorthand to retrieve reference to the verified ticket signature
626    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    /// Deconstructs self back into the unverified [Ticket].
634    pub fn leak(self) -> Ticket {
635        self.0
636    }
637
638    /// Creates new unacknowledged ticket from the [VerifiedTicket],
639    /// given our own part of the PoR challenge.
640    pub fn into_unacknowledged(self, own_key: HalfKey) -> UnacknowledgedTicket {
641        UnacknowledgedTicket { ticket: self, own_key }
642    }
643
644    /// Shorthand to acknowledge the ticket if the matching response is already known.
645    /// This is used upon receiving an aggregated ticket.
646    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
673/// Decodes [0x00000000000000, 0xffffffffffffff] to [0.0f64, 1.0f64]
674pub 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    // project interval [0x0fffffffffffff, 0x0000000000000f] to [0x00000000000010, 0x10000000000000]
689    let significand: u64 = tmp + 1;
690
691    f64::from_bits((1023u64 << 52) | (significand >> 4)) - 1.0
692}
693
694/// Encodes [0.0f64, 1.0f64] to [0x00000000000000, 0xffffffffffffff]
695pub 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    // // clear sign and exponent
713    let significand: u64 = tmp & 0x000fffffffffffffu64;
714
715    // project interval [0x10000000000000, 0x00000000000010] to [0x0000000000000f, 0x0fffffffffffff]
716    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/// Represents a [VerifiedTicket] with an unknown other part of the [HalfKey].
725/// Once the other [HalfKey] is known (forming a [Response]),
726/// it can be [acknowledged](UnacknowledgedTicket::acknowledge).
727#[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    /// Convenience method to retrieve a reference to the underlying verified [Ticket].
735    #[inline]
736    pub fn verified_ticket(&self) -> &Ticket {
737        self.ticket.verified_ticket()
738    }
739
740    /// Verifies that the given acknowledgement solves this ticket's challenge and then
741    /// turns this unacknowledged ticket into an acknowledged ticket by adding
742    /// the received acknowledgement of the forwarded packet.
743    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/// Status of the acknowledged ticket.
756#[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    /// The ticket is available for redeeming or aggregating
774    #[default]
775    Untouched = 0,
776    /// Ticket is currently being redeemed in and ongoing redemption process
777    BeingRedeemed = 1,
778    /// Ticket is currently being aggregated in and ongoing aggregation process
779    BeingAggregated = 2,
780}
781
782/// Contains acknowledgment information and the respective ticket
783#[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    /// Convenience method to retrieve a reference to the underlying verified [Ticket].
805    #[inline]
806    pub fn verified_ticket(&self) -> &Ticket {
807        self.ticket.verified_ticket()
808    }
809
810    /// Checks if this acknowledged ticket is winning.
811    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    /// Transforms this ticket into [RedeemableTicket] that can be redeemed on-chain
816    /// or transformed into [TransferableWinningTicket] that can be sent for aggregation.
817    /// The `chain_keypair` must not be of the ticket's issuer.
818    pub fn into_redeemable(
819        self,
820        chain_keypair: &ChainKeypair,
821        domain_separator: &Hash,
822    ) -> crate::errors::Result<RedeemableTicket> {
823        // This function must be called by ticket recipient and not the issuer
824        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    /// Shorthand for transforming this ticket into [TransferableWinningTicket].
839    /// See [`AcknowledgedTicket::into_redeemable`] for details.
840    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/// Represents a winning ticket that can be successfully redeemed on chain.
857#[derive(Clone, Debug, Serialize, Deserialize)]
858pub struct RedeemableTicket {
859    /// Verified ticket that can be redeemed.
860    pub ticket: VerifiedTicket,
861    /// Solution to the PoR challenge in the ticket.
862    pub response: Response,
863    /// VRF parameters required for redeeming.
864    pub vrf_params: VrfParameters,
865    /// Channel domain separator used to compute the VRF parameters.
866    pub channel_dst: Hash,
867}
868
869impl RedeemableTicket {
870    /// Convenience method to retrieve a reference to the underlying verified [Ticket].
871    #[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/// Represents a ticket that could be transferred over the wire
900/// and independently verified again by the other party.
901///
902/// The [TransferableWinningTicket] can be easily retrieved from [RedeemableTicket], which strips
903/// information about verification.
904/// [TransferableWinningTicket] can be attempted to be converted back to [RedeemableTicket] only
905/// when verified via [`TransferableWinningTicket::into_redeemable`] again.
906#[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    /// Attempts to transform this ticket back into a [RedeemableTicket].
916    ///
917    /// Verifies that the `signer` matches the `expected_issuer` and that the
918    /// ticket has valid signature from the `signer`.
919    /// Then it verifies if the ticket is winning and therefore if it can be successfully
920    /// redeemed on-chain.
921    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; // 100 %
1207        let price_per_packet: U256 = 10000000000000000u128.into(); // 0.01 HOPR
1208        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}