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, Copy, Debug, PartialEq, Eq)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99pub struct SurbReceiverInfo(#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] [u8; Self::SIZE]);
100
101impl SurbReceiverInfo {
102 pub fn new(pov: ProofOfRelayValues, share: [u8; 32]) -> Self {
103 let mut ret = [0u8; Self::SIZE];
104 ret[0..ProofOfRelayValues::SIZE].copy_from_slice(&pov.0);
105 ret[ProofOfRelayValues::SIZE..ProofOfRelayValues::SIZE + 32].copy_from_slice(&share);
107 Self(ret)
108 }
109
110 pub fn proof_of_relay_values(&self) -> ProofOfRelayValues {
111 ProofOfRelayValues::try_from(&self.0[0..ProofOfRelayValues::SIZE])
112 .expect("SurbReceiverInfo always contains valid ProofOfRelayValues")
113 }
114}
115
116impl AsRef<[u8]> for SurbReceiverInfo {
117 fn as_ref(&self) -> &[u8] {
118 &self.0
119 }
120}
121
122impl<'a> TryFrom<&'a [u8]> for SurbReceiverInfo {
123 type Error = GeneralError;
124
125 fn try_from(value: &'a [u8]) -> std::result::Result<Self, Self::Error> {
126 value
127 .try_into()
128 .map(Self)
129 .map_err(|_| GeneralError::ParseError("SurbReceiverInfo".into()))
130 }
131}
132
133impl BytesRepresentable for SurbReceiverInfo {
134 const SIZE: usize = ProofOfRelayValues::SIZE + 32;
135}
136
137#[derive(Clone, PartialEq, Eq)]
140pub struct ProofOfRelayString([u8; Self::SIZE]);
141
142impl ProofOfRelayString {
143 fn new(next_ticket_challenge: &EthereumChallenge, hint: &HalfKeyChallenge) -> Self {
144 let mut ret = [0u8; Self::SIZE];
145 ret[0..EthereumChallenge::SIZE].copy_from_slice(next_ticket_challenge.as_ref());
146 ret[EthereumChallenge::SIZE..].copy_from_slice(hint.as_ref());
147 Self(ret)
148 }
149
150 pub fn next_ticket_challenge(&self) -> EthereumChallenge {
152 EthereumChallenge(Address::new(&self.0[0..EthereumChallenge::SIZE]))
153 }
154
155 pub fn acknowledgement_challenge_or_hint(&self) -> HalfKeyChallenge {
158 HalfKeyChallenge::new(&self.0[EthereumChallenge::SIZE..])
159 }
160}
161
162impl std::fmt::Debug for ProofOfRelayString {
163 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
164 f.debug_tuple("ProofOfRelayString")
165 .field(&const_hex::encode(&self.0[0..EthereumChallenge::SIZE]))
166 .field(&const_hex::encode(&self.0[EthereumChallenge::SIZE..]))
167 .finish()
168 }
169}
170
171impl TryFrom<&[u8]> for ProofOfRelayString {
172 type Error = GeneralError;
173
174 fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
175 value
176 .try_into()
177 .map(Self)
178 .map_err(|_| GeneralError::ParseError("ProofOfRelayString".into()))
179 }
180}
181
182impl AsRef<[u8]> for ProofOfRelayString {
183 fn as_ref(&self) -> &[u8] {
184 &self.0
185 }
186}
187impl BytesRepresentable for ProofOfRelayString {
188 const SIZE: usize = EthereumChallenge::SIZE + HalfKeyChallenge::SIZE;
189}
190
191#[derive(Clone)]
194pub struct ProofOfRelayOutput {
195 pub own_key: HalfKey,
196 pub next_ticket_challenge: EthereumChallenge,
197 pub ack_challenge: HalfKeyChallenge,
198}
199
200#[instrument(level = "trace", skip_all, err)]
208pub fn pre_verify(
209 secret: &SharedSecret,
210 pors: &ProofOfRelayString,
211 challenge: &EthereumChallenge,
212) -> Result<ProofOfRelayOutput> {
213 let own_key = derive_own_key_share(secret);
214 let own_share = own_key.to_challenge()?;
215
216 if Challenge::from_hint_and_share(&own_share, &pors.acknowledgement_challenge_or_hint())?
217 .to_ethereum_challenge()
218 .eq(challenge)
219 {
220 Ok(ProofOfRelayOutput {
221 next_ticket_challenge: pors.next_ticket_challenge(),
222 ack_challenge: pors.acknowledgement_challenge_or_hint(),
223 own_key,
224 })
225 } else {
226 Err(PacketError::PoRVerificationError)
227 }
228}
229
230pub fn generate_proof_of_relay(secrets: &[SharedSecret]) -> Result<(Vec<ProofOfRelayString>, ProofOfRelayValues)> {
232 let mut last_ack_key_share = None;
233 let mut por_strings = Vec::with_capacity(secrets.len());
234 let mut por_values = None;
235
236 for i in 0..secrets.len() {
237 let hint = last_ack_key_share
238 .unwrap_or_else(|| {
239 derive_ack_key_share(&secrets[i]) })
241 .to_challenge()?;
242
243 let next_ticket_challenge = if let Some(next_secret) = secrets.get(i + 1) {
244 let s1 = derive_own_key_share(&secrets[i]); let s2 = derive_ack_key_share(next_secret); last_ack_key_share = Some(s2);
248
249 Response::from_half_keys(&s1, &s2)? .to_challenge()?
251 .to_ethereum_challenge()
252 } else {
253 EthereumChallenge(hopr_types::crypto_random::random_bytes::<{ Address::SIZE }>().into())
254 };
255
256 if i > 0 {
257 por_strings.push(ProofOfRelayString::new(&next_ticket_challenge, &hint));
258 } else {
259 por_values = Some(ProofOfRelayValues::new(
260 secrets.len() as u8,
261 &hint,
262 &next_ticket_challenge,
263 ));
264 }
265 }
266
267 Ok((
268 por_strings,
269 por_values.ok_or(PacketError::LogicError("no shared secrets".into()))?,
270 ))
271}
272
273#[cfg(test)]
274mod tests {
275 use hopr_types::crypto_random::Randomizable;
276
277 use super::*;
278
279 impl ProofOfRelayValues {
280 fn create(
281 secret_b: &SharedSecret,
282 secret_c: Option<&SharedSecret>,
283 chain_length: u8,
284 ) -> hopr_types::crypto::errors::Result<(Self, HalfKey)> {
285 let s0 = derive_own_key_share(secret_b);
286 let s1 = derive_ack_key_share(secret_c.unwrap_or(&SharedSecret::random()));
287
288 let ack_challenge = derive_ack_key_share(secret_b).to_challenge()?;
289 let ticket_challenge = Response::from_half_keys(&s0, &s1)?
290 .to_challenge()?
291 .to_ethereum_challenge();
292
293 Ok((Self::new(chain_length, &ack_challenge, &ticket_challenge), s0))
294 }
295 }
296 impl ProofOfRelayString {
297 fn create(
299 secret_c: &SharedSecret,
300 secret_d: Option<&SharedSecret>,
301 ) -> hopr_types::crypto::errors::Result<Self> {
302 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()?
308 .to_ethereum_challenge();
309
310 let hint = s0.to_challenge()?;
311 Ok(Self::new(&next_ticket_challenge, &hint))
312 }
313
314 fn from_shared_secrets(
317 secrets: &[SharedSecret],
318 ) -> hopr_types::crypto::errors::Result<Vec<ProofOfRelayString>> {
319 (1..secrets.len())
320 .map(|i| ProofOfRelayString::create(&secrets[i], secrets.get(i + 1)))
321 .collect()
322 }
323 }
324
325 fn validate_por_half_keys(ethereum_challenge: &EthereumChallenge, own_key: &HalfKey, ack: &HalfKey) -> bool {
327 Response::from_half_keys(own_key, ack)
328 .map(|response| validate_por_response(ethereum_challenge, &response))
329 .unwrap_or(false)
330 }
331
332 fn validate_por_response(ethereum_challenge: &EthereumChallenge, response: &Response) -> bool {
334 response
335 .to_challenge()
336 .is_ok_and(|c| c.to_ethereum_challenge().eq(ethereum_challenge))
337 }
338
339 fn validate_por_hint(ethereum_challenge: &EthereumChallenge, own_share: &HalfKeyChallenge, ack: &HalfKey) -> bool {
341 Challenge::from_own_share_and_half_key(own_share, ack)
342 .map(|c| c.to_ethereum_challenge().eq(ethereum_challenge))
343 .unwrap_or(false)
344 }
345
346 #[test]
347 fn test_generate_proof_of_relay() -> anyhow::Result<()> {
348 for hops in 0..=3 {
349 let secrets = (0..=hops).map(|_| SharedSecret::random()).collect::<Vec<_>>();
350
351 let por_strings = ProofOfRelayString::from_shared_secrets(&secrets)?;
352 let por_values = ProofOfRelayValues::create(&secrets[0], secrets.get(1), secrets.len() as u8)?.0;
353
354 let (gen_por_strings, gen_por_values) = generate_proof_of_relay(&secrets)?;
355
356 if hops > 0 {
358 assert_eq!(por_values, gen_por_values);
359 }
360
361 assert_eq!(por_strings.len(), gen_por_strings.len());
362
363 for i in 0..por_strings.len() {
364 assert_eq!(
365 por_strings[i].acknowledgement_challenge_or_hint(),
366 gen_por_strings[i].acknowledgement_challenge_or_hint()
367 );
368
369 if i != por_strings.len() - 1 {
371 assert_eq!(
372 por_strings[i].next_ticket_challenge(),
373 gen_por_strings[i].next_ticket_challenge()
374 );
375 }
376 }
377 }
378
379 Ok(())
380 }
381
382 #[test]
383 fn test_por_preverify_validate() -> anyhow::Result<()> {
384 const AMOUNT: usize = 4;
385
386 let secrets = (0..AMOUNT).map(|_| SharedSecret::random()).collect::<Vec<_>>();
387
388 let first_challenge = ProofOfRelayValues::create(&secrets[0], Some(&secrets[1]), secrets.len() as u8)?.0;
390
391 let first_por_string = ProofOfRelayString::create(&secrets[1], Some(&secrets[2]))?;
393
394 let second_por_string = ProofOfRelayString::create(&secrets[2], Some(&secrets[3]))?;
396
397 let first_challenge_eth = first_challenge.ticket_challenge();
399 let first_result = pre_verify(&secrets[0], &first_por_string, &first_challenge_eth)
400 .expect("First challenge must be plausible");
401
402 let expected_hkc = derive_ack_key_share(&secrets[1]).to_challenge()?;
403 assert_eq!(expected_hkc, first_result.ack_challenge);
404
405 let expected_pors = ProofOfRelayString::try_from(first_por_string.as_ref())?;
407 assert_eq!(
408 expected_pors.next_ticket_challenge(),
409 first_result.next_ticket_challenge,
410 "Forward logic must extract correct challenge for the next downstream node"
411 );
412
413 let first_ack = derive_ack_key_share(&secrets[1]);
415 assert!(
416 validate_por_half_keys(&first_challenge.ticket_challenge(), &first_result.own_key, &first_ack),
417 "Acknowledgement must solve the challenge"
418 );
419
420 let first_result_challenge_eth = first_result.next_ticket_challenge;
422 let second_result = pre_verify(&secrets[1], &second_por_string, &first_result_challenge_eth)
423 .expect("Second challenge must be plausible");
424
425 let second_ack = derive_ack_key_share(&secrets[2]);
426 assert!(
427 validate_por_half_keys(&first_result.next_ticket_challenge, &second_result.own_key, &second_ack),
428 "Second acknowledgement must solve the challenge"
429 );
430
431 assert!(
432 validate_por_hint(
433 &first_result.next_ticket_challenge,
434 &second_result.own_key.to_challenge()?,
435 &second_ack
436 ),
437 "Second acknowledgement must solve the challenge"
438 );
439
440 Ok(())
441 }
442
443 #[test]
444 fn test_challenge_and_response_solving() -> anyhow::Result<()> {
445 const AMOUNT: usize = 2;
446 let secrets = (0..AMOUNT).map(|_| SharedSecret::random()).collect::<Vec<_>>();
447
448 let (first_challenge, own_key) =
449 ProofOfRelayValues::create(&secrets[0], Some(&secrets[1]), secrets.len() as u8)?;
450 let ack = derive_ack_key_share(&secrets[1]);
451
452 assert!(
453 validate_por_half_keys(&first_challenge.ticket_challenge(), &own_key, &ack),
454 "Challenge must be solved"
455 );
456
457 assert!(
458 validate_por_response(
459 &first_challenge.ticket_challenge(),
460 &Response::from_half_keys(&own_key, &ack)?
461 ),
462 "Returned response must solve the challenge"
463 );
464
465 Ok(())
466 }
467}