1use tracing::debug;
2
3use hopr_crypto_types::types::Hash;
4use hopr_internal_types::prelude::*;
5use hopr_primitive_types::prelude::*;
6
7use crate::errors::TicketValidationError;
8
9pub fn validate_unacknowledged_ticket(
12 ticket: Ticket,
13 channel: &ChannelEntry,
14 min_ticket_amount: Balance,
15 required_win_prob: f64,
16 unrealized_balance: Balance,
17 domain_separator: &Hash,
18) -> Result<VerifiedTicket, TicketValidationError> {
19 debug!(source = %channel.source, "validating unack ticket");
20
21 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 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 if !f64_approx_eq(
44 verified_ticket.win_prob(),
45 required_win_prob,
46 LOWEST_POSSIBLE_WINNING_PROB,
47 ) && verified_ticket.win_prob() < required_win_prob
48 {
49 return Err(TicketValidationError {
50 reason: format!(
51 "ticket winning probability {} is lower than required winning probability {required_win_prob}",
52 verified_ticket.win_prob()
53 ),
54 ticket: inner_ticket.clone().into(),
55 });
56 }
57
58 if channel.status == ChannelStatus::Closed {
60 return Err(TicketValidationError {
61 reason: format!("payment channel {} is not opened or pending to close", channel.get_id()),
62 ticket: inner_ticket.clone().into(),
63 });
64 }
65
66 if !channel.channel_epoch.eq(&inner_ticket.channel_epoch.into()) {
68 return Err(TicketValidationError {
69 reason: format!(
70 "ticket was created for a different channel iteration {} != {} of channel {}",
71 inner_ticket.channel_epoch,
72 channel.channel_epoch,
73 channel.get_id()
74 ),
75 ticket: inner_ticket.clone().into(),
76 });
77 }
78
79 if inner_ticket.amount.gt(&unrealized_balance) {
81 return Err(TicketValidationError {
82 reason: format!(
83 "ticket value {} is greater than remaining unrealized balance {unrealized_balance} for channel {}",
84 inner_ticket.amount,
85 channel.get_id()
86 ),
87 ticket: inner_ticket.clone().into(),
88 });
89 }
90
91 Ok(verified_ticket)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::validation::validate_unacknowledged_ticket;
98 use hex_literal::hex;
99 use hopr_crypto_types::prelude::*;
100 use lazy_static::lazy_static;
101 use std::ops::Add;
102
103 const SENDER_PRIV_BYTES: [u8; 32] = hex!("492057cf93e99b31d2a85bc5e98a9c3aa0021feec52c227cc8170e8f7d047775");
104 const TARGET_PRIV_BYTES: [u8; 32] = hex!("5bf21ea8cccd69aa784346b07bf79c84dac606e00eecaa68bf8c31aff397b1ca");
105
106 lazy_static! {
107 static ref SENDER_PRIV_KEY: ChainKeypair =
108 ChainKeypair::from_secret(&SENDER_PRIV_BYTES).expect("lazy static keypair should be valid");
109 static ref TARGET_PRIV_KEY: ChainKeypair =
110 ChainKeypair::from_secret(&TARGET_PRIV_BYTES).expect("lazy static keypair should be valid");
111 }
112
113 fn create_valid_ticket() -> anyhow::Result<Ticket> {
114 Ok(TicketBuilder::default()
115 .addresses(&*SENDER_PRIV_KEY, &*TARGET_PRIV_KEY)
116 .amount(1)
117 .index(1)
118 .index_offset(1)
119 .win_prob(1.0)
120 .channel_epoch(1)
121 .challenge(Default::default())
122 .build_signed(&SENDER_PRIV_KEY, &Hash::default())?
123 .leak())
124 }
125
126 fn create_channel_entry() -> ChannelEntry {
127 ChannelEntry::new(
128 SENDER_PRIV_KEY.public().to_address(),
129 TARGET_PRIV_KEY.public().to_address(),
130 Balance::new(100_u64, BalanceType::HOPR),
131 U256::zero(),
132 ChannelStatus::Open,
133 U256::one(),
134 )
135 }
136
137 #[async_std::test]
138 async fn test_ticket_validation_should_pass_if_ticket_ok() -> anyhow::Result<()> {
139 let ticket = create_valid_ticket()?;
140 let channel = create_channel_entry();
141
142 let more_than_ticket_balance = ticket.amount.add(&Balance::new(U256::from(500u128), BalanceType::HOPR));
143
144 let ret = validate_unacknowledged_ticket(
145 ticket,
146 &channel,
147 Balance::new(1_u64, BalanceType::HOPR),
148 1.0f64,
149 more_than_ticket_balance,
150 &Hash::default(),
151 );
152
153 assert!(ret.is_ok());
154
155 Ok(())
156 }
157
158 #[async_std::test]
159 async fn test_ticket_validation_should_fail_if_signer_not_sender() -> anyhow::Result<()> {
160 let ticket = create_valid_ticket()?;
161 let channel = create_channel_entry();
162
163 let ret = validate_unacknowledged_ticket(
164 ticket,
165 &channel,
166 Balance::new(1_u64, BalanceType::HOPR),
167 1.0f64,
168 Balance::zero(BalanceType::HOPR),
169 &Hash::default(),
170 );
171
172 assert!(ret.is_err());
173
174 Ok(())
175 }
176
177 #[async_std::test]
178 async fn test_ticket_validation_should_fail_if_ticket_amount_is_low() -> anyhow::Result<()> {
179 let ticket = create_valid_ticket()?;
180 let channel = create_channel_entry();
181
182 let ret = validate_unacknowledged_ticket(
183 ticket,
184 &channel,
185 Balance::new(2_u64, BalanceType::HOPR),
186 1.0f64,
187 Balance::zero(BalanceType::HOPR),
188 &Hash::default(),
189 );
190
191 assert!(ret.is_err());
192
193 Ok(())
194 }
195
196 #[async_std::test]
197 async fn test_ticket_validation_should_fail_if_ticket_chance_is_low() -> anyhow::Result<()> {
198 let mut ticket = create_valid_ticket()?;
199 ticket.encoded_win_prob = f64_to_win_prob(0.5f64)?;
200 let ticket = ticket
201 .sign(&SENDER_PRIV_KEY, &Hash::default())
202 .verified_ticket()
203 .clone();
204
205 let channel = create_channel_entry();
206
207 let ret = validate_unacknowledged_ticket(
208 ticket,
209 &channel,
210 Balance::new(1_u64, BalanceType::HOPR),
211 1.0_f64,
212 Balance::zero(BalanceType::HOPR),
213 &Hash::default(),
214 );
215
216 assert!(ret.is_err());
217
218 Ok(())
219 }
220
221 #[async_std::test]
222 async fn test_ticket_validation_should_fail_if_channel_is_closed() -> anyhow::Result<()> {
223 let ticket = create_valid_ticket()?;
224 let mut channel = create_channel_entry();
225 channel.status = ChannelStatus::Closed;
226
227 let ret = validate_unacknowledged_ticket(
228 ticket,
229 &channel,
230 Balance::new(1_u64, BalanceType::HOPR),
231 1.0_f64,
232 Balance::zero(BalanceType::HOPR),
233 &Hash::default(),
234 );
235
236 assert!(ret.is_err());
237
238 Ok(())
239 }
240
241 #[async_std::test]
242 async fn test_ticket_validation_should_fail_if_ticket_epoch_does_not_match_2() -> anyhow::Result<()> {
243 let mut ticket = create_valid_ticket()?;
244 ticket.channel_epoch = 2u32;
245 let ticket = ticket
246 .sign(&SENDER_PRIV_KEY, &Hash::default())
247 .verified_ticket()
248 .clone();
249
250 let channel = create_channel_entry();
251
252 let ret = validate_unacknowledged_ticket(
253 ticket,
254 &channel,
255 Balance::new(1_u64, BalanceType::HOPR),
256 1.0_f64,
257 Balance::zero(BalanceType::HOPR),
258 &Hash::default(),
259 );
260
261 assert!(ret.is_err());
262
263 Ok(())
264 }
265
266 #[async_std::test]
267 async fn test_ticket_validation_fail_if_does_not_have_funds() -> anyhow::Result<()> {
268 let ticket = create_valid_ticket()?;
269 let mut channel = create_channel_entry();
270 channel.balance = Balance::zero(BalanceType::HOPR);
271 channel.channel_epoch = U256::from(ticket.channel_epoch);
272
273 let ret = validate_unacknowledged_ticket(
274 ticket,
275 &channel,
276 Balance::new(1_u64, BalanceType::HOPR),
277 1.0_f64,
278 Balance::zero(BalanceType::HOPR),
279 &Hash::default(),
280 );
281
282 assert!(ret.is_err());
283
284 Ok(())
285 }
286}