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        })?;
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 verified_ticket.win_prob().approx_cmp(&required_win_prob).is_lt() {
44        return Err(TicketValidationError {
45            reason: format!(
46                "ticket winning probability {} is lower than required winning probability {required_win_prob}",
47                verified_ticket.win_prob()
48            ),
49            ticket: inner_ticket.clone().into(),
50        });
51    }
52
53    // The channel MUST be open or pending to close
54    if channel.status == ChannelStatus::Closed {
55        return Err(TicketValidationError {
56            reason: format!("payment channel {} is not opened or pending to close", channel.get_id()),
57            ticket: inner_ticket.clone().into(),
58        });
59    }
60
61    // The ticket's channelEpoch MUST match the current channel's epoch
62    if !channel.channel_epoch.eq(&inner_ticket.channel_epoch.into()) {
63        return Err(TicketValidationError {
64            reason: format!(
65                "ticket was created for a different channel iteration {} != {} of channel {}",
66                inner_ticket.channel_epoch,
67                channel.channel_epoch,
68                channel.get_id()
69            ),
70            ticket: inner_ticket.clone().into(),
71        });
72    }
73
74    // Ensure that the sender has enough funds
75    debug!(%unrealized_balance, channel_id = %channel.get_id(), "checking if sender has enough funds");
76    if inner_ticket.amount.gt(&unrealized_balance) {
77        return Err(TicketValidationError {
78            reason: format!(
79                "ticket value {} is greater than remaining unrealized balance {unrealized_balance} for channel {}",
80                inner_ticket.amount,
81                channel.get_id()
82            ),
83            ticket: inner_ticket.clone().into(),
84        });
85    }
86
87    Ok(verified_ticket)
88}
89
90#[cfg(test)]
91mod tests {
92    use std::ops::Add;
93
94    use hex_literal::hex;
95    use hopr_crypto_types::prelude::*;
96    use lazy_static::lazy_static;
97
98    use super::*;
99    use crate::validation::validate_unacknowledged_ticket;
100
101    const SENDER_PRIV_BYTES: [u8; 32] = hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775");
102    const TARGET_PRIV_BYTES: [u8; 32] = hex!("5bf21ea8cccd69aa784346b07bf79c84dac606e00eecaa68bf8c31aff397b1ca");
103
104    lazy_static! {
105        static ref SENDER_PRIV_KEY: ChainKeypair =
106            ChainKeypair::from_secret(&SENDER_PRIV_BYTES).expect("lazy static keypair should be valid");
107        static ref TARGET_PRIV_KEY: ChainKeypair =
108            ChainKeypair::from_secret(&TARGET_PRIV_BYTES).expect("lazy static keypair should be valid");
109    }
110
111    fn create_valid_ticket() -> anyhow::Result<Ticket> {
112        Ok(TicketBuilder::default()
113            .addresses(&*SENDER_PRIV_KEY, &*TARGET_PRIV_KEY)
114            .amount(1)
115            .index(1)
116            .index_offset(1)
117            .win_prob(1.0.try_into()?)
118            .channel_epoch(1)
119            .eth_challenge(Default::default())
120            .build_signed(&SENDER_PRIV_KEY, &Hash::default())?
121            .leak())
122    }
123
124    fn create_channel_entry() -> ChannelEntry {
125        ChannelEntry::new(
126            SENDER_PRIV_KEY.public().to_address(),
127            TARGET_PRIV_KEY.public().to_address(),
128            100.into(),
129            U256::zero(),
130            ChannelStatus::Open,
131            U256::one(),
132        )
133    }
134
135    #[tokio::test]
136    async fn test_ticket_validation_should_pass_if_ticket_ok() -> anyhow::Result<()> {
137        let ticket = create_valid_ticket()?;
138        let channel = create_channel_entry();
139
140        let more_than_ticket_balance = ticket.amount.add(500);
141
142        let ret = validate_unacknowledged_ticket(
143            ticket,
144            &channel,
145            1.into(),
146            1.0.try_into()?,
147            more_than_ticket_balance,
148            &Hash::default(),
149        );
150
151        assert!(ret.is_ok());
152
153        Ok(())
154    }
155
156    #[tokio::test]
157    async fn test_ticket_validation_should_fail_if_signer_not_sender() -> anyhow::Result<()> {
158        let ticket = create_valid_ticket()?;
159        let channel = create_channel_entry();
160
161        let ret = validate_unacknowledged_ticket(
162            ticket,
163            &channel,
164            1.into(),
165            1.0f64.try_into()?,
166            0.into(),
167            &Hash::default(),
168        );
169
170        assert!(ret.is_err());
171
172        Ok(())
173    }
174
175    #[tokio::test]
176    async fn test_ticket_validation_should_fail_if_ticket_amount_is_low() -> anyhow::Result<()> {
177        let ticket = create_valid_ticket()?;
178        let channel = create_channel_entry();
179
180        let ret =
181            validate_unacknowledged_ticket(ticket, &channel, 2.into(), 1.0.try_into()?, 0.into(), &Hash::default());
182
183        assert!(ret.is_err());
184
185        Ok(())
186    }
187
188    #[tokio::test]
189    async fn test_ticket_validation_should_fail_if_ticket_chance_is_low() -> anyhow::Result<()> {
190        let mut ticket = create_valid_ticket()?;
191        ticket.encoded_win_prob = WinningProbability::try_from(0.5f64)?.into();
192        let ticket = ticket
193            .sign(&SENDER_PRIV_KEY, &Hash::default())
194            .verified_ticket()
195            .clone();
196
197        let channel = create_channel_entry();
198
199        let ret =
200            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
201
202        assert!(ret.is_err());
203
204        Ok(())
205    }
206
207    #[tokio::test]
208    async fn test_ticket_validation_should_fail_if_channel_is_closed() -> anyhow::Result<()> {
209        let ticket = create_valid_ticket()?;
210        let mut channel = create_channel_entry();
211        channel.status = ChannelStatus::Closed;
212
213        let ret =
214            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
215
216        assert!(ret.is_err());
217
218        Ok(())
219    }
220
221    #[tokio::test]
222    async fn test_ticket_validation_should_fail_if_ticket_epoch_does_not_match_2() -> anyhow::Result<()> {
223        let mut ticket = create_valid_ticket()?;
224        ticket.channel_epoch = 2u32;
225        let ticket = ticket
226            .sign(&SENDER_PRIV_KEY, &Hash::default())
227            .verified_ticket()
228            .clone();
229
230        let channel = create_channel_entry();
231
232        let ret =
233            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
234
235        assert!(ret.is_err());
236
237        Ok(())
238    }
239
240    #[tokio::test]
241    async fn test_ticket_validation_fail_if_does_not_have_funds() -> anyhow::Result<()> {
242        let ticket = create_valid_ticket()?;
243        let mut channel = create_channel_entry();
244        channel.balance = 0.into();
245        channel.channel_epoch = U256::from(ticket.channel_epoch);
246
247        let ret =
248            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
249
250        assert!(ret.is_err());
251
252        Ok(())
253    }
254}