hopr_crypto_packet/
validation.rs

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