hopr_crypto_packet/
validation.rs

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