1use std::fmt::Formatter;
2
3use hopr_types::{crypto::prelude::*, primitive::prelude::*};
4use tracing::instrument;
5
6use crate::{
7 errors::{PacketError, Result},
8 sphinx::prelude::SharedSecret,
9};
10
11const HASH_KEY_OWN_KEY: &str = "HASH_KEY_OWN_KEY";
12const HASH_KEY_ACK_KEY: &str = "HASH_KEY_ACK_KEY";
13
14fn derive_own_key_share(secret: &SecretKey) -> HalfKey {
17 sample_secp256k1_field_element(secret.as_ref(), HASH_KEY_OWN_KEY).expect("failed to sample own key share")
18}
19
20pub fn derive_ack_key_share(secret: &SecretKey) -> HalfKey {
23 sample_secp256k1_field_element(secret.as_ref(), HASH_KEY_ACK_KEY).expect("failed to sample ack key share")
24}
25
26#[derive(Clone, Copy, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub struct ProofOfRelayValues(#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] [u8; Self::SIZE]);
32
33impl ProofOfRelayValues {
34 fn new(chain_len: u8, ack_challenge: &HalfKeyChallenge, ticket_challenge: &EthereumChallenge) -> Self {
35 let mut ret = [0u8; Self::SIZE];
36 ret[0] = chain_len;
37 ret[1..1 + HalfKeyChallenge::SIZE].copy_from_slice(ack_challenge.as_ref());
38 ret[1 + HalfKeyChallenge::SIZE..].copy_from_slice(ticket_challenge.as_ref());
39 Self(ret)
40 }
41
42 pub fn chain_length(&self) -> u8 {
45 self.0[0]
46 }
47
48 pub fn acknowledgement_challenge(&self) -> HalfKeyChallenge {
53 HalfKeyChallenge::new(&self.0[1..1 + HalfKeyChallenge::SIZE])
54 }
55
56 pub fn ticket_challenge(&self) -> EthereumChallenge {
59 EthereumChallenge(Address::new(&self.0[1 + HalfKeyChallenge::SIZE..]))
60 }
61}
62
63impl std::fmt::Debug for ProofOfRelayValues {
64 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65 f.debug_tuple("ProofOfRelayValues")
66 .field(&self.chain_length())
67 .field(&const_hex::encode(&self.0[1..1 + HalfKeyChallenge::SIZE]))
68 .field(&const_hex::encode(&self.0[1 + HalfKeyChallenge::SIZE..]))
69 .finish()
70 }
71}
72
73impl AsRef<[u8]> for ProofOfRelayValues {
74 fn as_ref(&self) -> &[u8] {
75 &self.0
76 }
77}
78
79impl<'a> TryFrom<&'a [u8]> for ProofOfRelayValues {
80 type Error = GeneralError;
81
82 fn try_from(value: &'a [u8]) -> std::result::Result<Self, Self::Error> {
83 value
84 .try_into()
85 .map(Self)
86 .map_err(|_| GeneralError::ParseError("ProofOfRelayValues".into()))
87 }
88}
89
90impl BytesRepresentable for ProofOfRelayValues {
91 const SIZE: usize = 1 + HalfKeyChallenge::SIZE + EthereumChallenge::SIZE;
92}
93
94#[derive(Clone, PartialEq, Eq)]
97pub struct ProofOfRelayString([u8; Self::SIZE]);
98
99impl ProofOfRelayString {
100 fn new(next_ticket_challenge: &EthereumChallenge, hint: &HalfKeyChallenge) -> Self {
101 let mut ret = [0u8; Self::SIZE];
102 ret[0..EthereumChallenge::SIZE].copy_from_slice(next_ticket_challenge.as_ref());
103 ret[EthereumChallenge::SIZE..].copy_from_slice(hint.as_ref());
104 Self(ret)
105 }
106
107 pub fn next_ticket_challenge(&self) -> EthereumChallenge {
109 EthereumChallenge(Address::new(&self.0[0..EthereumChallenge::SIZE]))
110 }
111
112 pub fn acknowledgement_challenge_or_hint(&self) -> HalfKeyChallenge {
115 HalfKeyChallenge::new(&self.0[EthereumChallenge::SIZE..])
116 }
117}
118
119impl std::fmt::Debug for ProofOfRelayString {
120 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
121 f.debug_tuple("ProofOfRelayString")
122 .field(&const_hex::encode(&self.0[0..EthereumChallenge::SIZE]))
123 .field(&const_hex::encode(&self.0[EthereumChallenge::SIZE..]))
124 .finish()
125 }
126}
127
128impl TryFrom<&[u8]> for ProofOfRelayString {
129 type Error = GeneralError;
130
131 fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
132 value
133 .try_into()
134 .map(Self)
135 .map_err(|_| GeneralError::ParseError("ProofOfRelayString".into()))
136 }
137}
138
139impl AsRef<[u8]> for ProofOfRelayString {
140 fn as_ref(&self) -> &[u8] {
141 &self.0
142 }
143}
144impl BytesRepresentable for ProofOfRelayString {
145 const SIZE: usize = EthereumChallenge::SIZE + HalfKeyChallenge::SIZE;
146}
147
148#[derive(Clone)]
151pub struct ProofOfRelayOutput {
152 pub own_key: HalfKey,
153 pub next_ticket_challenge: EthereumChallenge,
154 pub ack_challenge: HalfKeyChallenge,
155}
156
157#[instrument(level = "trace", skip_all, err)]
165pub fn pre_verify(
166 secret: &SharedSecret,
167 pors: &ProofOfRelayString,
168 challenge: &EthereumChallenge,
169) -> Result<ProofOfRelayOutput> {
170 let own_key = derive_own_key_share(secret);
171 let own_share = own_key.to_challenge()?;
172
173 if Challenge::from_hint_and_share(&own_share, &pors.acknowledgement_challenge_or_hint())?
174 .to_ethereum_challenge()
175 .eq(challenge)
176 {
177 Ok(ProofOfRelayOutput {
178 next_ticket_challenge: pors.next_ticket_challenge(),
179 ack_challenge: pors.acknowledgement_challenge_or_hint(),
180 own_key,
181 })
182 } else {
183 Err(PacketError::PoRVerificationError)
184 }
185}
186
187pub type ProofOfRelayValuesWithSolution = (ProofOfRelayValues, HalfKey);
192
193pub fn generate_proof_of_relay(
195 secrets: &[SharedSecret],
196) -> Result<(Vec<ProofOfRelayString>, ProofOfRelayValuesWithSolution)> {
197 if secrets.is_empty() {
198 return Err(PacketError::LogicError("no shared secrets".into()));
199 }
200
201 let mut last_ack_key_share = None;
202 let mut por_strings = Vec::with_capacity(secrets.len());
203 let mut por_values = None;
204
205 let first_ack_key_share = derive_ack_key_share(&secrets[0]); for i in 0..secrets.len() {
208 let hint = last_ack_key_share.unwrap_or(first_ack_key_share).to_challenge()?;
209
210 let next_ticket_challenge = if let Some(next_secret) = secrets.get(i + 1) {
211 let s1 = derive_own_key_share(&secrets[i]); let s2 = derive_ack_key_share(next_secret); last_ack_key_share = Some(s2);
215
216 Response::from_half_keys(&s1, &s2)? .to_challenge()?
218 .to_ethereum_challenge()
219 } else {
220 EthereumChallenge(hopr_types::crypto_random::random_bytes::<{ Address::SIZE }>().into())
224 };
225
226 if i > 0 {
227 por_strings.push(ProofOfRelayString::new(&next_ticket_challenge, &hint));
228 } else {
229 por_values = Some(ProofOfRelayValues::new(
230 secrets.len() as u8,
231 &hint,
232 &next_ticket_challenge,
233 ));
234 }
235 }
236
237 Ok((
238 por_strings,
239 (
240 por_values.expect("there must be shared secrets at this point"),
242 first_ack_key_share,
243 ),
244 ))
245}
246
247#[cfg(test)]
248mod tests {
249 use hopr_types::crypto_random::Randomizable;
250
251 use super::*;
252
253 impl ProofOfRelayValues {
254 fn create(
255 secret_b: &SharedSecret,
256 secret_c: Option<&SharedSecret>,
257 chain_length: u8,
258 ) -> hopr_types::crypto::errors::Result<(Self, HalfKey)> {
259 let s0 = derive_own_key_share(secret_b);
260 let s1 = derive_ack_key_share(secret_c.unwrap_or(&SharedSecret::random()));
261
262 let ack_challenge = derive_ack_key_share(secret_b).to_challenge()?;
263 let ticket_challenge = Response::from_half_keys(&s0, &s1)?
264 .to_challenge()?
265 .to_ethereum_challenge();
266
267 Ok((Self::new(chain_length, &ack_challenge, &ticket_challenge), s0))
268 }
269 }
270 impl ProofOfRelayString {
271 fn create(
273 secret_c: &SharedSecret,
274 secret_d: Option<&SharedSecret>,
275 ) -> hopr_types::crypto::errors::Result<Self> {
276 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()?
282 .to_ethereum_challenge();
283
284 let hint = s0.to_challenge()?;
285 Ok(Self::new(&next_ticket_challenge, &hint))
286 }
287
288 fn from_shared_secrets(
291 secrets: &[SharedSecret],
292 ) -> hopr_types::crypto::errors::Result<Vec<ProofOfRelayString>> {
293 (1..secrets.len())
294 .map(|i| ProofOfRelayString::create(&secrets[i], secrets.get(i + 1)))
295 .collect()
296 }
297 }
298
299 fn validate_por_half_keys(ethereum_challenge: &EthereumChallenge, own_key: &HalfKey, ack: &HalfKey) -> bool {
301 Response::from_half_keys(own_key, ack)
302 .map(|response| validate_por_response(ethereum_challenge, &response))
303 .unwrap_or(false)
304 }
305
306 fn validate_por_response(ethereum_challenge: &EthereumChallenge, response: &Response) -> bool {
308 response
309 .to_challenge()
310 .is_ok_and(|c| c.to_ethereum_challenge().eq(ethereum_challenge))
311 }
312
313 fn validate_por_hint(ethereum_challenge: &EthereumChallenge, own_share: &HalfKeyChallenge, ack: &HalfKey) -> bool {
315 Challenge::from_own_share_and_half_key(own_share, ack)
316 .map(|c| c.to_ethereum_challenge().eq(ethereum_challenge))
317 .unwrap_or(false)
318 }
319
320 #[test]
321 fn test_generate_proof_of_relay() -> anyhow::Result<()> {
322 for hops in 0..=3 {
323 let secrets = (0..=hops).map(|_| SharedSecret::random()).collect::<Vec<_>>();
324
325 let por_strings = ProofOfRelayString::from_shared_secrets(&secrets)?;
326 let por_values = ProofOfRelayValues::create(&secrets[0], secrets.get(1), secrets.len() as u8)?.0;
327
328 let (gen_por_strings, (gen_por_values, gen_por_sol)) = generate_proof_of_relay(&secrets)?;
329
330 assert_eq!(gen_por_sol, derive_ack_key_share(&secrets[0]));
333
334 assert_eq!(gen_por_values.chain_length(), (hops + 1) as u8);
337
338 assert_eq!(gen_por_values.acknowledgement_challenge(), gen_por_sol.to_challenge()?);
340
341 if hops > 0 {
343 assert_eq!(por_values, gen_por_values);
345
346 assert!(validate_por_half_keys(
349 &gen_por_values.ticket_challenge(),
350 &derive_own_key_share(&secrets[0]),
351 &derive_ack_key_share(&secrets[1])
352 ));
353
354 let res = pre_verify(&secrets[0], &gen_por_strings[0], &gen_por_values.ticket_challenge())?;
356 assert_eq!(res.next_ticket_challenge, gen_por_strings[0].next_ticket_challenge());
358
359 assert_eq!(
361 res.ack_challenge,
362 gen_por_strings[0].acknowledgement_challenge_or_hint()
363 );
364
365 assert_eq!(res.own_key, derive_own_key_share(&secrets[0]));
367 }
368
369 assert_eq!(por_strings.len(), gen_por_strings.len());
371
372 for i in 0..por_strings.len() {
373 assert_eq!(
375 por_strings[i].acknowledgement_challenge_or_hint(),
376 gen_por_strings[i].acknowledgement_challenge_or_hint()
377 );
378 assert_eq!(
380 gen_por_strings[i].acknowledgement_challenge_or_hint(),
381 derive_ack_key_share(&secrets[i + 1]).to_challenge()?
382 );
383
384 if i != por_strings.len() - 1 {
386 assert_eq!(
388 por_strings[i].next_ticket_challenge(),
389 gen_por_strings[i].next_ticket_challenge()
390 );
391
392 assert!(validate_por_half_keys(
395 &gen_por_strings[i].next_ticket_challenge(),
396 &derive_own_key_share(&secrets[i + 1]),
397 &derive_ack_key_share(&secrets[i + 2])
398 ));
399
400 let res = pre_verify(
403 &secrets[i + 1],
404 &gen_por_strings[i + 1],
405 &gen_por_strings[i].next_ticket_challenge(),
406 )?;
407 assert_eq!(
409 res.next_ticket_challenge,
410 gen_por_strings[i + 1].next_ticket_challenge()
411 );
412 assert_eq!(
414 res.ack_challenge,
415 gen_por_strings[i + 1].acknowledgement_challenge_or_hint()
416 );
417 assert_eq!(res.own_key, derive_own_key_share(&secrets[i + 1]));
419 }
420 }
421 }
422
423 Ok(())
424 }
425
426 #[test]
427 fn test_por_preverify_validate() -> anyhow::Result<()> {
428 const AMOUNT: usize = 4;
429
430 let secrets = (0..AMOUNT).map(|_| SharedSecret::random()).collect::<Vec<_>>();
431
432 let first_challenge = ProofOfRelayValues::create(&secrets[0], Some(&secrets[1]), secrets.len() as u8)?.0;
434
435 let first_por_string = ProofOfRelayString::create(&secrets[1], Some(&secrets[2]))?;
437
438 let second_por_string = ProofOfRelayString::create(&secrets[2], Some(&secrets[3]))?;
440
441 let first_challenge_eth = first_challenge.ticket_challenge();
443 let first_result = pre_verify(&secrets[0], &first_por_string, &first_challenge_eth)
444 .expect("First challenge must be plausible");
445
446 let expected_hkc = derive_ack_key_share(&secrets[1]).to_challenge()?;
447 assert_eq!(expected_hkc, first_result.ack_challenge);
448
449 let expected_pors = ProofOfRelayString::try_from(first_por_string.as_ref())?;
451 assert_eq!(
452 expected_pors.next_ticket_challenge(),
453 first_result.next_ticket_challenge,
454 "Forward logic must extract correct challenge for the next downstream node"
455 );
456
457 let first_ack = derive_ack_key_share(&secrets[1]);
459 assert!(
460 validate_por_half_keys(&first_challenge.ticket_challenge(), &first_result.own_key, &first_ack),
461 "Acknowledgement must solve the challenge"
462 );
463
464 let first_result_challenge_eth = first_result.next_ticket_challenge;
466 let second_result = pre_verify(&secrets[1], &second_por_string, &first_result_challenge_eth)
467 .expect("Second challenge must be plausible");
468
469 let second_ack = derive_ack_key_share(&secrets[2]);
470 assert!(
471 validate_por_half_keys(&first_result.next_ticket_challenge, &second_result.own_key, &second_ack),
472 "Second acknowledgement must solve the challenge"
473 );
474
475 assert!(
476 validate_por_hint(
477 &first_result.next_ticket_challenge,
478 &second_result.own_key.to_challenge()?,
479 &second_ack
480 ),
481 "Second acknowledgement must solve the challenge"
482 );
483
484 Ok(())
485 }
486
487 #[test]
488 fn test_challenge_and_response_solving() -> anyhow::Result<()> {
489 const AMOUNT: usize = 2;
490 let secrets = (0..AMOUNT).map(|_| SharedSecret::random()).collect::<Vec<_>>();
491
492 let (first_challenge, own_key) =
493 ProofOfRelayValues::create(&secrets[0], Some(&secrets[1]), secrets.len() as u8)?;
494 let ack = derive_ack_key_share(&secrets[1]);
495
496 assert!(
497 validate_por_half_keys(&first_challenge.ticket_challenge(), &own_key, &ack),
498 "Challenge must be solved"
499 );
500
501 assert!(
502 validate_por_response(
503 &first_challenge.ticket_challenge(),
504 &Response::from_half_keys(&own_key, &ack)?
505 ),
506 "Returned response must solve the challenge"
507 );
508
509 Ok(())
510 }
511}