hopr_crypto_packet/
validation.rs1use hopr_crypto_types::types::Hash;
2use hopr_internal_types::prelude::*;
3use hopr_primitive_types::prelude::*;
4use tracing::debug;
5
6use crate::errors::TicketValidationError;
7
8pub 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 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 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 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 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 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 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}