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's index MUST be greater or equal than the channel's ticketIndex
67    if inner_ticket.index < channel.ticket_index {
68        return Err(TicketValidationError {
69            kind: ValidationErrorKind::IndexTooLow(channel.ticket_index),
70            ticket: (*inner_ticket).into(),
71            issuer: Some(*verified_ticket.verified_issuer()),
72        });
73    }
74
75    // Ensure that the sender has enough funds
76    debug!(%unrealized_balance, channel_id = %channel.get_id(), "checking if sender has enough funds");
77    if inner_ticket.amount.gt(&unrealized_balance) {
78        return Err(TicketValidationError {
79            kind: ValidationErrorKind::InsufficientFunds(*channel.get_id(), unrealized_balance),
80            ticket: (*inner_ticket).into(),
81            issuer: Some(*verified_ticket.verified_issuer()),
82        });
83    }
84
85    Ok(verified_ticket)
86}
87
88#[cfg(test)]
89mod tests {
90    use std::ops::Add;
91
92    use hex_literal::hex;
93    use hopr_types::crypto::prelude::*;
94    use lazy_static::lazy_static;
95
96    use super::*;
97    use crate::validation::validate_unacknowledged_ticket;
98
99    const SENDER_PRIV_BYTES: [u8; 32] = hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775");
100    const TARGET_PRIV_BYTES: [u8; 32] = hex!("5bf21ea8cccd69aa784346b07bf79c84dac606e00eecaa68bf8c31aff397b1ca");
101
102    lazy_static! {
103        static ref SENDER_PRIV_KEY: ChainKeypair =
104            ChainKeypair::from_secret(&SENDER_PRIV_BYTES).expect("lazy static keypair should be valid");
105        static ref TARGET_PRIV_KEY: ChainKeypair =
106            ChainKeypair::from_secret(&TARGET_PRIV_BYTES).expect("lazy static keypair should be valid");
107    }
108
109    fn create_valid_ticket() -> anyhow::Result<Ticket> {
110        Ok(TicketBuilder::default()
111            .counterparty(&*TARGET_PRIV_KEY)
112            .amount(1)
113            .index(1)
114            .win_prob(1.0.try_into()?)
115            .channel_epoch(1)
116            .eth_challenge(Default::default())
117            .build_signed(&SENDER_PRIV_KEY, &Hash::default())?
118            .leak())
119    }
120
121    fn create_channel_entry() -> ChannelEntry {
122        ChannelEntry::builder()
123            .between(&*SENDER_PRIV_KEY, &*TARGET_PRIV_KEY)
124            .amount(100)
125            .ticket_index(0)
126            .status(ChannelStatus::Open)
127            .epoch(1)
128            .build()
129            .unwrap()
130    }
131
132    #[tokio::test]
133    async fn test_ticket_validation_should_pass_if_ticket_ok() -> anyhow::Result<()> {
134        let ticket = create_valid_ticket()?;
135        let channel = create_channel_entry();
136
137        let more_than_ticket_balance = ticket.amount.add(500);
138
139        let ret = validate_unacknowledged_ticket(
140            ticket,
141            &channel,
142            1.into(),
143            1.0.try_into()?,
144            more_than_ticket_balance,
145            &Hash::default(),
146        );
147
148        assert!(ret.is_ok());
149
150        Ok(())
151    }
152
153    #[tokio::test]
154    async fn test_ticket_validation_should_fail_if_signer_not_sender() -> anyhow::Result<()> {
155        let ticket = create_valid_ticket()?;
156        let channel = create_channel_entry();
157
158        let ret = validate_unacknowledged_ticket(
159            ticket,
160            &channel,
161            1.into(),
162            1.0f64.try_into()?,
163            0.into(),
164            &Hash::default(),
165        );
166
167        assert!(ret.is_err());
168
169        Ok(())
170    }
171
172    #[tokio::test]
173    async fn test_ticket_validation_should_fail_if_ticket_amount_is_low() -> anyhow::Result<()> {
174        let ticket = create_valid_ticket()?;
175        let channel = create_channel_entry();
176
177        let ret =
178            validate_unacknowledged_ticket(ticket, &channel, 2.into(), 1.0.try_into()?, 0.into(), &Hash::default());
179
180        assert!(ret.is_err());
181
182        Ok(())
183    }
184
185    #[tokio::test]
186    async fn test_ticket_validation_should_fail_if_ticket_chance_is_low() -> anyhow::Result<()> {
187        let mut ticket = create_valid_ticket()?;
188        ticket.encoded_win_prob = WinningProbability::try_from(0.5f64)?.into();
189        let ticket = *ticket.sign(&SENDER_PRIV_KEY, &Hash::default()).verified_ticket();
190
191        let channel = create_channel_entry();
192
193        let ret =
194            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
195
196        assert!(ret.is_err());
197
198        Ok(())
199    }
200
201    #[tokio::test]
202    async fn test_ticket_validation_should_fail_if_channel_is_closed() -> anyhow::Result<()> {
203        let ticket = create_valid_ticket()?;
204        let mut channel = create_channel_entry();
205        channel.status = ChannelStatus::Closed;
206
207        let ret =
208            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
209
210        assert!(ret.is_err());
211
212        Ok(())
213    }
214
215    #[tokio::test]
216    async fn test_ticket_validation_should_fail_if_ticket_epoch_does_not_match_2() -> anyhow::Result<()> {
217        let mut ticket = create_valid_ticket()?;
218        ticket.channel_epoch = 2u32;
219        let ticket = *ticket.sign(&SENDER_PRIV_KEY, &Hash::default()).verified_ticket();
220
221        let channel = create_channel_entry();
222
223        let ret =
224            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
225
226        assert!(ret.is_err());
227
228        Ok(())
229    }
230
231    #[tokio::test]
232    async fn test_ticket_validation_fail_if_does_not_have_funds() -> anyhow::Result<()> {
233        let ticket = create_valid_ticket()?;
234        let mut channel = create_channel_entry();
235        channel.balance = 0.into();
236        channel.channel_epoch = ticket.channel_epoch;
237
238        let ret =
239            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
240
241        assert!(ret.is_err());
242
243        Ok(())
244    }
245
246    #[tokio::test]
247    async fn test_ticket_validation_fail_when_index_is_lower_than_channel_index() -> anyhow::Result<()> {
248        let ticket = create_valid_ticket()?;
249        let mut channel = create_channel_entry();
250        channel.ticket_index = 2;
251        channel.channel_epoch = ticket.channel_epoch;
252
253        assert!(ticket.index < channel.ticket_index);
254
255        let ret =
256            validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
257
258        assert!(ret.is_err());
259
260        Ok(())
261    }
262}