1use hopr_types::{crypto::types::Hash, internal::prelude::*, primitive::prelude::*};
2use tracing::{debug, instrument};
3
4use crate::errors::{TicketValidationError, ValidationErrorKind};
5
6#[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 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 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 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 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 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 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 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}