hopr_internal_types/
tickets.rs

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