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.sign(&SENDER_PRIV_KEY, &Hash::default()).verified_ticket();
198
199 let channel = create_channel_entry();
200
201 let ret =
202 validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
203
204 assert!(ret.is_err());
205
206 Ok(())
207 }
208
209 #[tokio::test]
210 async fn test_ticket_validation_should_fail_if_channel_is_closed() -> anyhow::Result<()> {
211 let ticket = create_valid_ticket()?;
212 let mut channel = create_channel_entry();
213 channel.status = ChannelStatus::Closed;
214
215 let ret =
216 validate_unacknowledged_ticket(ticket, &channel, 1.into(), 1.0.try_into()?, 0.into(), &Hash::default());
217
218 assert!(ret.is_err());
219
220 Ok(())
221 }
222
223 #[tokio::test]
224 async fn test_ticket_validation_should_fail_if_ticket_epoch_does_not_match_2() -> anyhow::Result<()> {
225 let mut ticket = create_valid_ticket()?;
226 ticket.channel_epoch = 2u32;
227 let ticket = *ticket.sign(&SENDER_PRIV_KEY, &Hash::default()).verified_ticket();
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 = 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}