Skip to main content

hopr_crypto_packet/
validation.rs

1use hopr_types::{crypto::types::Hash, internal::prelude::*, primitive::prelude::*};
2use tracing::{debug, instrument};
3
4use crate::errors::{TicketValidationError, ValidationErrorKind};
5
6/// Performs validations of the given unacknowledged ticket and channel.
7/// This is a higher-level function, hence it is not in `hopr-internal-types` crate.
8#[instrument(level = "trace", skip_all, err)]
9pub fn validate_unacknowledged_ticket(
10    ticket: Ticket,
11    channel: &ChannelEntry,
12    min_ticket_amount: HoprBalance,
13    required_win_prob: WinningProbability,
14    unrealized_balance: HoprBalance,
15    domain_separator: &Hash,
16) -> Result<VerifiedTicket, TicketValidationError> {
17    debug!(source = %channel.source, %ticket, "validating unacknowledged ticket");
18
19    // The ticket signer MUST be the sender
20    let verified_ticket = ticket
21        .verify(&channel.source, domain_separator)
22        .map_err(|ticket| TicketValidationError {
23            kind: ValidationErrorKind::InvalidSigner,
24            ticket,
25            issuer: None,
26        })?;
27
28    let inner_ticket = verified_ticket.verified_ticket();
29
30    // The ticket amount MUST be greater or equal to min_ticket_amount
31    if !inner_ticket.amount.ge(&min_ticket_amount) {
32        return Err(TicketValidationError {
33            kind: ValidationErrorKind::LowValue(min_ticket_amount),
34            ticket: (*inner_ticket).into(),
35            issuer: Some(*verified_ticket.verified_issuer()),
36        });
37    }
38
39    // The ticket must have at least the required winning probability
40    if verified_ticket.win_prob().approx_cmp(&required_win_prob).is_lt() {
41        return Err(TicketValidationError {
42            kind: ValidationErrorKind::LowWinProb(required_win_prob),
43            ticket: (*inner_ticket).into(),
44            issuer: Some(*verified_ticket.verified_issuer()),
45        });
46    }
47
48    // The channel MUST be open or pending to close
49    if channel.status == ChannelStatus::Closed {
50        return Err(TicketValidationError {
51            kind: ValidationErrorKind::ChannelClosed(*channel.get_id()),
52            ticket: (*inner_ticket).into(),
53            issuer: Some(*verified_ticket.verified_issuer()),
54        });
55    }
56
57    // The ticket's channelEpoch MUST match the current channel's epoch
58    if channel.channel_epoch != inner_ticket.channel_epoch {
59        return Err(TicketValidationError {
60            kind: ValidationErrorKind::EpochMismatch(channel.channel_epoch),
61            ticket: (*inner_ticket).into(),
62            issuer: Some(*verified_ticket.verified_issuer()),
63        });
64    }
65
66    // The ticket counterparty MUST be the channel's destination, otherwise the
67    // ticket is unredeemable by construction (its channel id would not match
68    // any real channel) yet it would still be accepted and relayed here.
69    if inner_ticket.counterparty != channel.destination {
70        return Err(TicketValidationError {
71            kind: ValidationErrorKind::WrongCounterparty(channel.destination),
72            ticket: (*inner_ticket).into(),
73            issuer: Some(*verified_ticket.verified_issuer()),
74        });
75    }
76
77    // The ticket's index MUST be greater or equal than the channel's ticketIndex
78    if inner_ticket.index < channel.ticket_index {
79        return Err(TicketValidationError {
80            kind: ValidationErrorKind::IndexTooLow(channel.ticket_index),
81            ticket: (*inner_ticket).into(),
82            issuer: Some(*verified_ticket.verified_issuer()),
83        });
84    }
85
86    // Ensure that the sender has enough funds
87    debug!(%unrealized_balance, channel_id = %channel.get_id(), "checking if sender has enough funds");
88    if inner_ticket.amount.gt(&unrealized_balance) {
89        return Err(TicketValidationError {
90            kind: ValidationErrorKind::InsufficientFunds(*channel.get_id(), unrealized_balance),
91            ticket: (*inner_ticket).into(),
92            issuer: Some(*verified_ticket.verified_issuer()),
93        });
94    }
95
96    Ok(verified_ticket)
97}
98
99#[cfg(test)]
100mod tests {
101    use std::ops::Add;
102
103    use anyhow::Context;
104    use hex_literal::hex;
105    use hopr_types::crypto::prelude::*;
106    use lazy_static::lazy_static;
107
108    use super::*;
109    use crate::validation::validate_unacknowledged_ticket;
110
111    const SENDER_PRIV_BYTES: [u8; 32] = hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775");
112    const TARGET_PRIV_BYTES: [u8; 32] = hex!("5bf21ea8cccd69aa784346b07bf79c84dac606e00eecaa68bf8c31aff397b1ca");
113
114    lazy_static! {
115        static ref SENDER_PRIV_KEY: ChainKeypair =
116            ChainKeypair::from_secret(&SENDER_PRIV_BYTES).expect("lazy static keypair should be valid");
117        static ref TARGET_PRIV_KEY: ChainKeypair =
118            ChainKeypair::from_secret(&TARGET_PRIV_BYTES).expect("lazy static keypair should be valid");
119    }
120
121    fn create_valid_ticket() -> anyhow::Result<Ticket> {
122        Ok(TicketBuilder::default()
123            .counterparty(&*TARGET_PRIV_KEY)
124            .amount(1)
125            .index(1)
126            .win_prob(1.0.try_into()?)
127            .channel_epoch(1)
128            .eth_challenge(Default::default())
129            .build_signed(&SENDER_PRIV_KEY, &Hash::default())?
130            .leak())
131    }
132
133    fn create_channel_entry() -> ChannelEntry {
134        ChannelEntry::builder()
135            .between(&*SENDER_PRIV_KEY, &*TARGET_PRIV_KEY)
136            .amount(100)
137            .ticket_index(0)
138            .status(ChannelStatus::Open)
139            .epoch(1)
140            .build()
141            .unwrap()
142    }
143
144    #[tokio::test]
145    async fn test_ticket_validation_should_pass_if_ticket_ok() -> anyhow::Result<()> {
146        let ticket = create_valid_ticket()?;
147        let channel = create_channel_entry();
148
149        let more_than_ticket_balance = ticket.amount.add(500);
150
151        let ret = validate_unacknowledged_ticket(
152            ticket,
153            &channel,
154            1.into(),
155            1.0.try_into()?,
156            more_than_ticket_balance,
157            &Hash::default(),
158        );
159
160        assert!(ret.is_ok());
161
162        Ok(())
163    }
164
165    #[tokio::test]
166    async fn test_ticket_validation_should_fail_if_signer_not_sender() -> anyhow::Result<()> {
167        let ticket = create_valid_ticket()?;
168        let channel = create_channel_entry();
169
170        let ret = validate_unacknowledged_ticket(
171            ticket,
172            &channel,
173            1.into(),
174            1.0f64.try_into()?,
175            0.into(),
176            &Hash::default(),
177        );
178
179        assert!(ret.is_err());
180
181        Ok(())
182    }
183
184    #[tokio::test]
185    async fn test_ticket_validation_should_fail_if_ticket_amount_is_low() -> anyhow::Result<()> {
186        let ticket = create_valid_ticket()?;
187        let channel = create_channel_entry();
188
189        let ret =
190            validate_unacknowledged_ticket(ticket, &channel, 2.into(), 1.0.try_into()?, 0.into(), &Hash::default());
191
192        assert!(ret.is_err());
193
194        Ok(())
195    }
196
197    #[tokio::test]
198    async fn test_ticket_validation_should_fail_if_ticket_chance_is_low() -> anyhow::Result<()> {
199        let mut ticket = create_valid_ticket()?;
200        ticket.encoded_win_prob = WinningProbability::try_from(0.5f64)?.into();
201        let ticket = *ticket.sign(&SENDER_PRIV_KEY, &Hash::default()).verified_ticket();
202
203        let channel = create_channel_entry();
204
205        let ret =
206            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
207
208        assert!(ret.is_err());
209
210        Ok(())
211    }
212
213    #[tokio::test]
214    async fn test_ticket_validation_should_fail_if_channel_is_closed() -> anyhow::Result<()> {
215        let ticket = create_valid_ticket()?;
216        let mut channel = create_channel_entry();
217        channel.status = ChannelStatus::Closed;
218
219        let ret =
220            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
221
222        assert!(ret.is_err());
223
224        Ok(())
225    }
226
227    #[tokio::test]
228    async fn test_ticket_validation_should_fail_if_ticket_epoch_does_not_match_2() -> anyhow::Result<()> {
229        let mut ticket = create_valid_ticket()?;
230        ticket.channel_epoch = 2u32;
231        let ticket = *ticket.sign(&SENDER_PRIV_KEY, &Hash::default()).verified_ticket();
232
233        let channel = create_channel_entry();
234
235        let ret =
236            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
237
238        assert!(ret.is_err());
239
240        Ok(())
241    }
242
243    #[tokio::test]
244    async fn test_ticket_validation_fail_if_does_not_have_funds() -> anyhow::Result<()> {
245        let ticket = create_valid_ticket()?;
246        let mut channel = create_channel_entry();
247        channel.balance = 0.into();
248        channel.channel_epoch = ticket.channel_epoch;
249
250        let ret =
251            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
252
253        assert!(ret.is_err());
254
255        Ok(())
256    }
257
258    #[tokio::test]
259    async fn test_ticket_validation_fail_when_index_is_lower_than_channel_index() -> anyhow::Result<()> {
260        let ticket = create_valid_ticket()?;
261        let mut channel = create_channel_entry();
262        channel.ticket_index = 2;
263        channel.channel_epoch = ticket.channel_epoch;
264
265        assert!(ticket.index < channel.ticket_index);
266
267        let ret =
268            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
269
270        assert!(ret.is_err());
271
272        Ok(())
273    }
274
275    #[tokio::test]
276    async fn ticket_validation_should_pass_when_index_equals_channel_index() -> anyhow::Result<()> {
277        let ticket = create_valid_ticket()?;
278        let mut channel = create_channel_entry();
279        channel.ticket_index = ticket.index;
280        channel.channel_epoch = ticket.channel_epoch;
281
282        let ret = validate_unacknowledged_ticket(
283            ticket,
284            &channel,
285            1.into(),
286            1.0.try_into()?,
287            100.into(),
288            &Hash::default(),
289        );
290
291        assert!(
292            ret.is_ok(),
293            "ticket with index == channel.ticket_index must be accepted, got: {:?}",
294            ret
295        );
296
297        Ok(())
298    }
299
300    #[tokio::test]
301    async fn ticket_validation_should_fail_if_counterparty_is_not_channel_destination() -> anyhow::Result<()> {
302        let bogus_counterparty = ChainKeypair::random();
303        let ticket = TicketBuilder::default()
304            .counterparty(&bogus_counterparty)
305            .amount(1)
306            .index(1)
307            .win_prob(1.0.try_into().context("should convert win prob")?)
308            .channel_epoch(1)
309            .eth_challenge(Default::default())
310            .build_signed(&SENDER_PRIV_KEY, &Hash::default())
311            .context("should build signed ticket")?
312            .leak();
313
314        let channel = create_channel_entry();
315        assert_ne!(ticket.counterparty, channel.destination);
316
317        let ret = validate_unacknowledged_ticket(
318            ticket,
319            &channel,
320            1.into(),
321            1.0.try_into().context("should convert required win prob")?,
322            100.into(),
323            &Hash::default(),
324        );
325
326        let error = ret.expect_err("ticket with bogus counterparty must be rejected");
327        assert!(
328            matches!(error.kind, ValidationErrorKind::WrongCounterparty(_)),
329            "expected WrongCounterparty, got: {:?}",
330            error.kind
331        );
332
333        Ok(())
334    }
335}