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