hopr_crypto_packet/
validation.rs

1use hopr_crypto_types::types::Hash;
2use hopr_internal_types::prelude::*;
3use hopr_primitive_types::prelude::*;
4use tracing::{debug, instrument};
5
6use crate::errors::TicketValidationError;
7
8/// Performs validations of the given unacknowledged ticket and channel.
9/// This is a higher-level function, hence it is not in `hopr-internal-types` crate.
10#[instrument(level = "trace", skip_all, err)]
11pub fn validate_unacknowledged_ticket(
12    ticket: Ticket,
13    channel: &ChannelEntry,
14    min_ticket_amount: HoprBalance,
15    required_win_prob: WinningProbability,
16    unrealized_balance: HoprBalance,
17    domain_separator: &Hash,
18) -> Result<VerifiedTicket, TicketValidationError> {
19    debug!(source = %channel.source, %ticket, "validating unacknowledged ticket");
20
21    // The ticket signer MUST be the sender
22    let verified_ticket = ticket
23        .verify(&channel.source, domain_separator)
24        .map_err(|ticket| TicketValidationError {
25            reason: format!("ticket signer does not match the sender: {ticket}"),
26            ticket,
27            issuer: None,
28        })?;
29
30    let inner_ticket = verified_ticket.verified_ticket();
31
32    // The ticket amount MUST be greater or equal to min_ticket_amount
33    if !inner_ticket.amount.ge(&min_ticket_amount) {
34        return Err(TicketValidationError {
35            reason: format!(
36                "ticket amount {} in not at least {min_ticket_amount}",
37                inner_ticket.amount
38            ),
39            ticket: (*inner_ticket).into(),
40            issuer: Some(*verified_ticket.verified_issuer()),
41        });
42    }
43
44    // The ticket must have at least the required winning probability
45    if verified_ticket.win_prob().approx_cmp(&required_win_prob).is_lt() {
46        return Err(TicketValidationError {
47            reason: format!(
48                "ticket winning probability {} is lower than required winning probability {required_win_prob}",
49                verified_ticket.win_prob()
50            ),
51            ticket: (*inner_ticket).into(),
52            issuer: Some(*verified_ticket.verified_issuer()),
53        });
54    }
55
56    // The channel MUST be open or pending to close
57    if channel.status == ChannelStatus::Closed {
58        return Err(TicketValidationError {
59            reason: format!("payment channel {} is not opened or pending to close", channel.get_id()),
60            ticket: (*inner_ticket).into(),
61            issuer: Some(*verified_ticket.verified_issuer()),
62        });
63    }
64
65    // The ticket's channelEpoch MUST match the current channel's epoch
66    if channel.channel_epoch != inner_ticket.channel_epoch {
67        return Err(TicketValidationError {
68            reason: format!(
69                "ticket was created for a different channel iteration {} != {} of channel {}",
70                inner_ticket.channel_epoch,
71                channel.channel_epoch,
72                channel.get_id()
73            ),
74            ticket: (*inner_ticket).into(),
75            issuer: Some(*verified_ticket.verified_issuer()),
76        });
77    }
78
79    // Ensure that the sender has enough funds
80    debug!(%unrealized_balance, channel_id = %channel.get_id(), "checking if sender has enough funds");
81    if inner_ticket.amount.gt(&unrealized_balance) {
82        return Err(TicketValidationError {
83            reason: format!(
84                "ticket value {} is greater than remaining unrealized balance {unrealized_balance} for channel {}",
85                inner_ticket.amount,
86                channel.get_id()
87            ),
88            ticket: (*inner_ticket).into(),
89            issuer: Some(*verified_ticket.verified_issuer()),
90        });
91    }
92
93    Ok(verified_ticket)
94}
95
96#[cfg(test)]
97mod tests {
98    use std::ops::Add;
99
100    use hex_literal::hex;
101    use hopr_crypto_types::prelude::*;
102    use lazy_static::lazy_static;
103
104    use super::*;
105    use crate::validation::validate_unacknowledged_ticket;
106
107    const SENDER_PRIV_BYTES: [u8; 32] = hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775");
108    const TARGET_PRIV_BYTES: [u8; 32] = hex!("5bf21ea8cccd69aa784346b07bf79c84dac606e00eecaa68bf8c31aff397b1ca");
109
110    lazy_static! {
111        static ref SENDER_PRIV_KEY: ChainKeypair =
112            ChainKeypair::from_secret(&SENDER_PRIV_BYTES).expect("lazy static keypair should be valid");
113        static ref TARGET_PRIV_KEY: ChainKeypair =
114            ChainKeypair::from_secret(&TARGET_PRIV_BYTES).expect("lazy static keypair should be valid");
115    }
116
117    fn create_valid_ticket() -> anyhow::Result<Ticket> {
118        Ok(TicketBuilder::default()
119            .counterparty(&*TARGET_PRIV_KEY)
120            .amount(1)
121            .index(1)
122            .win_prob(1.0.try_into()?)
123            .channel_epoch(1)
124            .eth_challenge(Default::default())
125            .build_signed(&SENDER_PRIV_KEY, &Hash::default())?
126            .leak())
127    }
128
129    fn create_channel_entry() -> ChannelEntry {
130        ChannelEntry::new(
131            SENDER_PRIV_KEY.public().to_address(),
132            TARGET_PRIV_KEY.public().to_address(),
133            100.into(),
134            0,
135            ChannelStatus::Open,
136            1,
137        )
138    }
139
140    #[tokio::test]
141    async fn test_ticket_validation_should_pass_if_ticket_ok() -> anyhow::Result<()> {
142        let ticket = create_valid_ticket()?;
143        let channel = create_channel_entry();
144
145        let more_than_ticket_balance = ticket.amount.add(500);
146
147        let ret = validate_unacknowledged_ticket(
148            ticket,
149            &channel,
150            1.into(),
151            1.0.try_into()?,
152            more_than_ticket_balance,
153            &Hash::default(),
154        );
155
156        assert!(ret.is_ok());
157
158        Ok(())
159    }
160
161    #[tokio::test]
162    async fn test_ticket_validation_should_fail_if_signer_not_sender() -> anyhow::Result<()> {
163        let ticket = create_valid_ticket()?;
164        let channel = create_channel_entry();
165
166        let ret = validate_unacknowledged_ticket(
167            ticket,
168            &channel,
169            1.into(),
170            1.0f64.try_into()?,
171            0.into(),
172            &Hash::default(),
173        );
174
175        assert!(ret.is_err());
176
177        Ok(())
178    }
179
180    #[tokio::test]
181    async fn test_ticket_validation_should_fail_if_ticket_amount_is_low() -> anyhow::Result<()> {
182        let ticket = create_valid_ticket()?;
183        let channel = create_channel_entry();
184
185        let ret =
186            validate_unacknowledged_ticket(ticket, &channel, 2.into(), 1.0.try_into()?, 0.into(), &Hash::default());
187
188        assert!(ret.is_err());
189
190        Ok(())
191    }
192
193    #[tokio::test]
194    async fn test_ticket_validation_should_fail_if_ticket_chance_is_low() -> anyhow::Result<()> {
195        let mut ticket = create_valid_ticket()?;
196        ticket.encoded_win_prob = WinningProbability::try_from(0.5f64)?.into();
197        let ticket = ticket
198            .sign(&SENDER_PRIV_KEY, &Hash::default())
199            .verified_ticket()
200            .clone();
201
202        let channel = create_channel_entry();
203
204        let ret =
205            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
206
207        assert!(ret.is_err());
208
209        Ok(())
210    }
211
212    #[tokio::test]
213    async fn test_ticket_validation_should_fail_if_channel_is_closed() -> anyhow::Result<()> {
214        let ticket = create_valid_ticket()?;
215        let mut channel = create_channel_entry();
216        channel.status = ChannelStatus::Closed;
217
218        let ret =
219            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
220
221        assert!(ret.is_err());
222
223        Ok(())
224    }
225
226    #[tokio::test]
227    async fn test_ticket_validation_should_fail_if_ticket_epoch_does_not_match_2() -> anyhow::Result<()> {
228        let mut ticket = create_valid_ticket()?;
229        ticket.channel_epoch = 2u32;
230        let ticket = ticket
231            .sign(&SENDER_PRIV_KEY, &Hash::default())
232            .verified_ticket()
233            .clone();
234
235        let channel = create_channel_entry();
236
237        let ret =
238            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
239
240        assert!(ret.is_err());
241
242        Ok(())
243    }
244
245    #[tokio::test]
246    async fn test_ticket_validation_fail_if_does_not_have_funds() -> anyhow::Result<()> {
247        let ticket = create_valid_ticket()?;
248        let mut channel = create_channel_entry();
249        channel.balance = 0.into();
250        channel.channel_epoch = ticket.channel_epoch;
251
252        let ret =
253            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
254
255        assert!(ret.is_err());
256
257        Ok(())
258    }
259}