1use std::fmt::Formatter;
2
3use hopr_crypto_sphinx::prelude::SharedSecret;
4use hopr_crypto_types::prelude::*;
5use hopr_primitive_types::prelude::*;
6use tracing::instrument;
7
8use crate::errors::{PacketError, Result};
9
10const HASH_KEY_OWN_KEY: &str = "HASH_KEY_OWN_KEY";
11const HASH_KEY_ACK_KEY: &str = "HASH_KEY_ACK_KEY";
12
13fn derive_own_key_share(secret: &SecretKey) -> HalfKey {
16 sample_secp256k1_field_element(secret.as_ref(), HASH_KEY_OWN_KEY).expect("failed to sample own key share")
17}
18
19pub fn derive_ack_key_share(secret: &SecretKey) -> HalfKey {
22 sample_secp256k1_field_element(secret.as_ref(), HASH_KEY_ACK_KEY).expect("failed to sample ack key share")
23}
24
25#[derive(Clone, Copy, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct ProofOfRelayValues(#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] [u8; Self::SIZE]);
31
32impl ProofOfRelayValues {
33 fn new(chain_len: u8, ack_challenge: &HalfKeyChallenge, ticket_challenge: &EthereumChallenge) -> Self {
34 let mut ret = [0u8; Self::SIZE];
35 ret[0] = chain_len;
36 ret[1..1 + HalfKeyChallenge::SIZE].copy_from_slice(ack_challenge.as_ref());
37 ret[1 + HalfKeyChallenge::SIZE..].copy_from_slice(ticket_challenge.as_ref());
38 Self(ret)
39 }
40
41 pub fn chain_length(&self) -> u8 {
44 self.0[0]
45 }
46
47 pub fn acknowledgement_challenge(&self) -> HalfKeyChallenge {
52 HalfKeyChallenge::new(&self.0[1..1 + HalfKeyChallenge::SIZE])
53 }
54
55 pub fn ticket_challenge(&self) -> EthereumChallenge {
58 EthereumChallenge(Address::new(&self.0[1 + HalfKeyChallenge::SIZE..]))
59 }
60}
61
62impl std::fmt::Debug for ProofOfRelayValues {
63 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
64 f.debug_tuple("ProofOfRelayValues")
65 .field(&self.chain_length())
66 .field(&hex::encode(&self.0[1..1 + HalfKeyChallenge::SIZE]))
67 .field(&hex::encode(&self.0[1 + HalfKeyChallenge::SIZE..]))
68 .finish()
69 }
70}
71
72impl AsRef<[u8]> for ProofOfRelayValues {
73 fn as_ref(&self) -> &[u8] {
74 &self.0
75 }
76}
77
78impl<'a> TryFrom<&'a [u8]> for ProofOfRelayValues {
79 type Error = GeneralError;
80
81 fn try_from(value: &'a [u8]) -> std::result::Result<Self, Self::Error> {
82 value
83 .try_into()
84 .map(Self)
85 .map_err(|_| GeneralError::ParseError("ProofOfRelayValues".into()))
86 }
87}
88
89impl BytesRepresentable for ProofOfRelayValues {
90 const SIZE: usize = 1 + HalfKeyChallenge::SIZE + EthereumChallenge::SIZE;
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub struct SurbReceiverInfo(#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] [u8; Self::SIZE]);
99
100impl SurbReceiverInfo {
101 pub fn new(pov: ProofOfRelayValues, share: [u8; 32]) -> Self {
102 let mut ret = [0u8; Self::SIZE];
103 ret[0..ProofOfRelayValues::SIZE].copy_from_slice(&pov.0);
104 ret[ProofOfRelayValues::SIZE..ProofOfRelayValues::SIZE + 32].copy_from_slice(&share);
106 Self(ret)
107 }
108
109 pub fn proof_of_relay_values(&self) -> ProofOfRelayValues {
110 ProofOfRelayValues::try_from(&self.0[0..ProofOfRelayValues::SIZE])
111 .expect("SurbReceiverInfo always contains valid ProofOfRelayValues")
112 }
113}
114
115impl AsRef<[u8]> for SurbReceiverInfo {
116 fn as_ref(&self) -> &[u8] {
117 &self.0
118 }
119}
120
121impl<'a> TryFrom<&'a [u8]> for SurbReceiverInfo {
122 type Error = GeneralError;
123
124 fn try_from(value: &'a [u8]) -> std::result::Result<Self, Self::Error> {
125 value
126 .try_into()
127 .map(Self)
128 .map_err(|_| GeneralError::ParseError("SurbReceiverInfo".into()))
129 }
130}
131
132impl BytesRepresentable for SurbReceiverInfo {
133 const SIZE: usize = ProofOfRelayValues::SIZE + 32;
134}
135
136#[derive(Clone, PartialEq, Eq)]
139pub struct ProofOfRelayString([u8; Self::SIZE]);
140
141impl ProofOfRelayString {
142 fn new(next_ticket_challenge: &EthereumChallenge, hint: &HalfKeyChallenge) -> Self {
143 let mut ret = [0u8; Self::SIZE];
144 ret[0..EthereumChallenge::SIZE].copy_from_slice(next_ticket_challenge.as_ref());
145 ret[EthereumChallenge::SIZE..].copy_from_slice(hint.as_ref());
146 Self(ret)
147 }
148
149 pub fn next_ticket_challenge(&self) -> EthereumChallenge {
151 EthereumChallenge(Address::new(&self.0[0..EthereumChallenge::SIZE]))
152 }
153
154 pub fn acknowledgement_challenge_or_hint(&self) -> HalfKeyChallenge {
157 HalfKeyChallenge::new(&self.0[EthereumChallenge::SIZE..])
158 }
159}
160
161impl std::fmt::Debug for ProofOfRelayString {
162 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
163 f.debug_tuple("ProofOfRelayString")
164 .field(&hex::encode(&self.0[0..EthereumChallenge::SIZE]))
165 .field(&hex::encode(&self.0[EthereumChallenge::SIZE..]))
166 .finish()
167 }
168}
169
170impl TryFrom<&[u8]> for ProofOfRelayString {
171 type Error = GeneralError;
172
173 fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
174 value
175 .try_into()
176 .map(Self)
177 .map_err(|_| GeneralError::ParseError("ProofOfRelayString".into()))
178 }
179}
180
181impl AsRef<[u8]> for ProofOfRelayString {
182 fn as_ref(&self) -> &[u8] {
183 &self.0
184 }
185}
186impl BytesRepresentable for ProofOfRelayString {
187 const SIZE: usize = EthereumChallenge::SIZE + HalfKeyChallenge::SIZE;
188}
189
190#[derive(Clone)]
193pub struct ProofOfRelayOutput {
194 pub own_key: HalfKey,
195 pub next_ticket_challenge: EthereumChallenge,
196 pub ack_challenge: HalfKeyChallenge,
197}
198
199#[instrument(level = "trace", skip_all, err)]
207pub fn pre_verify(
208 secret: &SharedSecret,
209 pors: &ProofOfRelayString,
210 challenge: &EthereumChallenge,
211) -> Result<ProofOfRelayOutput> {
212 let own_key = derive_own_key_share(secret);
213 let own_share = own_key.to_challenge()?;
214
215 if Challenge::from_hint_and_share(&own_share, &pors.acknowledgement_challenge_or_hint())?
216 .to_ethereum_challenge()
217 .eq(challenge)
218 {
219 Ok(ProofOfRelayOutput {
220 next_ticket_challenge: pors.next_ticket_challenge(),
221 ack_challenge: pors.acknowledgement_challenge_or_hint(),
222 own_key,
223 })
224 } else {
225 Err(PacketError::PoRVerificationError)
226 }
227}
228
229pub fn generate_proof_of_relay(secrets: &[SharedSecret]) -> Result<(Vec<ProofOfRelayString>, ProofOfRelayValues)> {
231 let mut last_ack_key_share = None;
232 let mut por_strings = Vec::with_capacity(secrets.len());
233 let mut por_values = None;
234
235 for i in 0..secrets.len() {
236 let hint = last_ack_key_share
237 .unwrap_or_else(|| {
238 derive_ack_key_share(&secrets[i]) })
240 .to_challenge()?;
241
242 let next_ticket_challenge = if let Some(next_secret) = secrets.get(i + 1) {
243 let s1 = derive_own_key_share(&secrets[i]); let s2 = derive_ack_key_share(next_secret); last_ack_key_share = Some(s2);
247
248 Response::from_half_keys(&s1, &s2)? .to_challenge()?
250 .to_ethereum_challenge()
251 } else {
252 EthereumChallenge(hopr_crypto_random::random_bytes::<{ Address::SIZE }>().into())
253 };
254
255 if i > 0 {
256 por_strings.push(ProofOfRelayString::new(&next_ticket_challenge, &hint));
257 } else {
258 por_values = Some(ProofOfRelayValues::new(
259 secrets.len() as u8,
260 &hint,
261 &next_ticket_challenge,
262 ));
263 }
264 }
265
266 Ok((
267 por_strings,
268 por_values.ok_or(PacketError::LogicError("no shared secrets".into()))?,
269 ))
270}
271
272#[cfg(test)]
273mod tests {
274 use hopr_crypto_random::Randomizable;
275
276 use super::*;
277
278 impl ProofOfRelayValues {
279 fn create(
280 secret_b: &SharedSecret,
281 secret_c: Option<&SharedSecret>,
282 chain_length: u8,
283 ) -> hopr_crypto_types::errors::Result<(Self, HalfKey)> {
284 let s0 = derive_own_key_share(secret_b);
285 let s1 = derive_ack_key_share(secret_c.unwrap_or(&SharedSecret::random()));
286
287 let ack_challenge = derive_ack_key_share(secret_b).to_challenge()?;
288 let ticket_challenge = Response::from_half_keys(&s0, &s1)?
289 .to_challenge()?
290 .to_ethereum_challenge();
291
292 Ok((Self::new(chain_length, &ack_challenge, &ticket_challenge), s0))
293 }
294 }
295 impl ProofOfRelayString {
296 fn create(secret_c: &SharedSecret, secret_d: Option<&SharedSecret>) -> hopr_crypto_types::errors::Result<Self> {
298 let s0 = derive_ack_key_share(secret_c); let s1 = derive_own_key_share(secret_c); let s2 = derive_ack_key_share(secret_d.unwrap_or(&SharedSecret::random())); let next_ticket_challenge = Response::from_half_keys(&s1, &s2)? .to_challenge()?
304 .to_ethereum_challenge();
305
306 let hint = s0.to_challenge()?;
307 Ok(Self::new(&next_ticket_challenge, &hint))
308 }
309
310 fn from_shared_secrets(secrets: &[SharedSecret]) -> hopr_crypto_types::errors::Result<Vec<ProofOfRelayString>> {
313 (1..secrets.len())
314 .map(|i| ProofOfRelayString::create(&secrets[i], secrets.get(i + 1)))
315 .collect()
316 }
317 }
318
319 fn validate_por_half_keys(ethereum_challenge: &EthereumChallenge, own_key: &HalfKey, ack: &HalfKey) -> bool {
321 Response::from_half_keys(own_key, ack)
322 .map(|response| validate_por_response(ethereum_challenge, &response))
323 .unwrap_or(false)
324 }
325
326 fn validate_por_response(ethereum_challenge: &EthereumChallenge, response: &Response) -> bool {
328 response
329 .to_challenge()
330 .is_ok_and(|c| c.to_ethereum_challenge().eq(ethereum_challenge))
331 }
332
333 fn validate_por_hint(ethereum_challenge: &EthereumChallenge, own_share: &HalfKeyChallenge, ack: &HalfKey) -> bool {
335 Challenge::from_own_share_and_half_key(own_share, ack)
336 .map(|c| c.to_ethereum_challenge().eq(ethereum_challenge))
337 .unwrap_or(false)
338 }
339
340 #[test]
341 fn test_generate_proof_of_relay() -> anyhow::Result<()> {
342 for hops in 0..=3 {
343 let secrets = (0..=hops).map(|_| SharedSecret::random()).collect::<Vec<_>>();
344
345 let por_strings = ProofOfRelayString::from_shared_secrets(&secrets)?;
346 let por_values = ProofOfRelayValues::create(&secrets[0], secrets.get(1), secrets.len() as u8)?.0;
347
348 let (gen_por_strings, gen_por_values) = generate_proof_of_relay(&secrets)?;
349
350 if hops > 0 {
352 assert_eq!(por_values, gen_por_values);
353 }
354
355 assert_eq!(por_strings.len(), gen_por_strings.len());
356
357 for i in 0..por_strings.len() {
358 assert_eq!(
359 por_strings[i].acknowledgement_challenge_or_hint(),
360 gen_por_strings[i].acknowledgement_challenge_or_hint()
361 );
362
363 if i != por_strings.len() - 1 {
365 assert_eq!(
366 por_strings[i].next_ticket_challenge(),
367 gen_por_strings[i].next_ticket_challenge()
368 );
369 }
370 }
371 }
372
373 Ok(())
374 }
375
376 #[test]
377 fn test_por_preverify_validate() -> anyhow::Result<()> {
378 const AMOUNT: usize = 4;
379
380 let secrets = (0..AMOUNT).map(|_| SharedSecret::random()).collect::<Vec<_>>();
381
382 let first_challenge = ProofOfRelayValues::create(&secrets[0], Some(&secrets[1]), secrets.len() as u8)?.0;
384
385 let first_por_string = ProofOfRelayString::create(&secrets[1], Some(&secrets[2]))?;
387
388 let second_por_string = ProofOfRelayString::create(&secrets[2], Some(&secrets[3]))?;
390
391 let first_challenge_eth = first_challenge.ticket_challenge();
393 let first_result = pre_verify(&secrets[0], &first_por_string, &first_challenge_eth)
394 .expect("First challenge must be plausible");
395
396 let expected_hkc = derive_ack_key_share(&secrets[1]).to_challenge()?;
397 assert_eq!(expected_hkc, first_result.ack_challenge);
398
399 let expected_pors = ProofOfRelayString::try_from(first_por_string.as_ref())?;
401 assert_eq!(
402 expected_pors.next_ticket_challenge(),
403 first_result.next_ticket_challenge,
404 "Forward logic must extract correct challenge for the next downstream node"
405 );
406
407 let first_ack = derive_ack_key_share(&secrets[1]);
409 assert!(
410 validate_por_half_keys(&first_challenge.ticket_challenge(), &first_result.own_key, &first_ack),
411 "Acknowledgement must solve the challenge"
412 );
413
414 let first_result_challenge_eth = first_result.next_ticket_challenge;
416 let second_result = pre_verify(&secrets[1], &second_por_string, &first_result_challenge_eth)
417 .expect("Second challenge must be plausible");
418
419 let second_ack = derive_ack_key_share(&secrets[2]);
420 assert!(
421 validate_por_half_keys(&first_result.next_ticket_challenge, &second_result.own_key, &second_ack),
422 "Second acknowledgement must solve the challenge"
423 );
424
425 assert!(
426 validate_por_hint(
427 &first_result.next_ticket_challenge,
428 &second_result.own_key.to_challenge()?,
429 &second_ack
430 ),
431 "Second acknowledgement must solve the challenge"
432 );
433
434 Ok(())
435 }
436
437 #[test]
438 fn test_challenge_and_response_solving() -> anyhow::Result<()> {
439 const AMOUNT: usize = 2;
440 let secrets = (0..AMOUNT).map(|_| SharedSecret::random()).collect::<Vec<_>>();
441
442 let (first_challenge, own_key) =
443 ProofOfRelayValues::create(&secrets[0], Some(&secrets[1]), secrets.len() as u8)?;
444 let ack = derive_ack_key_share(&secrets[1]);
445
446 assert!(
447 validate_por_half_keys(&first_challenge.ticket_challenge(), &own_key, &ack),
448 "Challenge must be solved"
449 );
450
451 assert!(
452 validate_por_response(
453 &first_challenge.ticket_challenge(),
454 &Response::from_half_keys(&own_key, &ack)?
455 ),
456 "Returned response must solve the challenge"
457 );
458
459 Ok(())
460 }
461}