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