hopr_crypto_packet/
por.rs

1use std::fmt::Formatter;
2
3use hopr_crypto_sphinx::prelude::SharedSecret;
4use hopr_crypto_types::{
5    crypto_traits::Randomizable,
6    prelude::{SecretKey, sample_secp256k1_field_element},
7    types::{Challenge, HalfKey, HalfKeyChallenge, Response},
8};
9use hopr_primitive_types::prelude::*;
10
11use crate::errors::{PacketError, Result};
12
13const HASH_KEY_OWN_KEY: &str = "HASH_KEY_OWN_KEY";
14const HASH_KEY_ACK_KEY: &str = "HASH_KEY_ACK_KEY";
15
16/// Used in Proof of Relay to derive own half-key (S0)
17/// The function samples a secp256k1 field element using the given `secret` via `sample_field_element`.
18fn derive_own_key_share(secret: &SecretKey) -> HalfKey {
19    sample_secp256k1_field_element(secret.as_ref(), HASH_KEY_OWN_KEY).expect("failed to sample own key share")
20}
21
22/// Used in Proof of Relay to derive the half-key of for the acknowledgement (S1)
23/// The function samples a secp256k1 field element using the given `secret` via `sample_field_element`.
24pub fn derive_ack_key_share(secret: &SecretKey) -> HalfKey {
25    sample_secp256k1_field_element(secret.as_ref(), HASH_KEY_ACK_KEY).expect("failed to sample ack key share")
26}
27
28/// Type that contains the challenge for the first ticket sent to the first relayer.
29///
30/// This is the first entry of the entire PoR challenge chain generated for the packet.
31#[derive(Clone, Copy, PartialEq, Eq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub struct ProofOfRelayValues(#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] [u8; Self::SIZE]);
34
35impl ProofOfRelayValues {
36    fn new(chain_len: u8, ack_challenge: &HalfKeyChallenge, ticket_challenge: &EthereumChallenge) -> Self {
37        let mut ret = [0u8; Self::SIZE];
38        ret[0] = chain_len;
39        ret[1..1 + HalfKeyChallenge::SIZE].copy_from_slice(ack_challenge.as_ref());
40        ret[1 + HalfKeyChallenge::SIZE..].copy_from_slice(ticket_challenge.as_ref());
41        Self(ret)
42    }
43
44    /// Length of this PoR challenge chain (number of hops + 1).
45    // TODO: needed to know how to price the ticket on the return path, will be fixed in #3765
46    pub fn chain_length(&self) -> u8 {
47        self.0[0]
48    }
49
50    /// Returns the challenge that must be solved once the acknowledgement
51    /// to the packet has been received.
52    ///
53    /// This is the [`ProofOfRelayValues::ticket_challenge`] minus the Hint.
54    pub fn acknowledgement_challenge(&self) -> HalfKeyChallenge {
55        HalfKeyChallenge::new(&self.0[1..1 + HalfKeyChallenge::SIZE])
56    }
57
58    /// Returns the complete challenge that is present on the ticket corresponding to the
59    /// packet.
60    pub fn ticket_challenge(&self) -> EthereumChallenge {
61        EthereumChallenge::new(&self.0[1 + HalfKeyChallenge::SIZE..])
62    }
63}
64
65impl std::fmt::Debug for ProofOfRelayValues {
66    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
67        f.debug_tuple("ProofOfRelayValues")
68            .field(&self.chain_length())
69            .field(&hex::encode(&self.0[1..1 + HalfKeyChallenge::SIZE]))
70            .field(&hex::encode(&self.0[1 + HalfKeyChallenge::SIZE..]))
71            .finish()
72    }
73}
74
75impl AsRef<[u8]> for ProofOfRelayValues {
76    fn as_ref(&self) -> &[u8] {
77        &self.0
78    }
79}
80
81impl<'a> TryFrom<&'a [u8]> for ProofOfRelayValues {
82    type Error = GeneralError;
83
84    fn try_from(value: &'a [u8]) -> std::result::Result<Self, Self::Error> {
85        value
86            .try_into()
87            .map(Self)
88            .map_err(|_| GeneralError::ParseError("ProofOfRelayValues".into()))
89    }
90}
91
92impl BytesRepresentable for ProofOfRelayValues {
93    const SIZE: usize = 1 + HalfKeyChallenge::SIZE + EthereumChallenge::SIZE;
94}
95
96/// Wraps the [`ProofOfRelayValues`] with some additional information about the sender of the packet,
97/// that is supposed to be passed along with the SURB.
98// TODO: currently 32 bytes are reserved for future use by Shamir's secret sharing scheme.
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct SurbReceiverInfo(#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] [u8; Self::SIZE]);
102
103impl SurbReceiverInfo {
104    pub fn new(pov: ProofOfRelayValues, share: [u8; 32]) -> Self {
105        let mut ret = [0u8; Self::SIZE];
106        ret[0..ProofOfRelayValues::SIZE].copy_from_slice(&pov.0);
107        // Share is currently not used but will be used in the future
108        ret[ProofOfRelayValues::SIZE..ProofOfRelayValues::SIZE + 32].copy_from_slice(&share);
109        Self(ret)
110    }
111
112    pub fn proof_of_relay_values(&self) -> ProofOfRelayValues {
113        ProofOfRelayValues::try_from(&self.0[0..ProofOfRelayValues::SIZE])
114            .expect("SurbReceiverInfo always contains valid ProofOfRelayValues")
115    }
116}
117
118impl AsRef<[u8]> for SurbReceiverInfo {
119    fn as_ref(&self) -> &[u8] {
120        &self.0
121    }
122}
123
124impl<'a> TryFrom<&'a [u8]> for SurbReceiverInfo {
125    type Error = GeneralError;
126
127    fn try_from(value: &'a [u8]) -> std::result::Result<Self, Self::Error> {
128        value
129            .try_into()
130            .map(Self)
131            .map_err(|_| GeneralError::ParseError("SurbReceiverInfo".into()))
132    }
133}
134
135impl BytesRepresentable for SurbReceiverInfo {
136    const SIZE: usize = ProofOfRelayValues::SIZE + 32;
137}
138
139/// Contains the Proof of Relay challenge for the next downstream node as well as the hint that is used to
140/// verify the challenge that is given to the relayer.
141#[derive(Clone, PartialEq, Eq)]
142pub struct ProofOfRelayString([u8; Self::SIZE]);
143
144impl ProofOfRelayString {
145    fn new(next_ticket_challenge: &EthereumChallenge, hint: &HalfKeyChallenge) -> Self {
146        let mut ret = [0u8; Self::SIZE];
147        ret[0..EthereumChallenge::SIZE].copy_from_slice(next_ticket_challenge.as_ref());
148        ret[EthereumChallenge::SIZE..].copy_from_slice(hint.as_ref());
149        Self(ret)
150    }
151
152    /// Challenge that must be printed on the ticket for the next downstream node.
153    pub fn next_ticket_challenge(&self) -> EthereumChallenge {
154        EthereumChallenge::new(&self.0[0..EthereumChallenge::SIZE])
155    }
156
157    /// Proof of Relay hint value for this node. In case this node is a sender
158    /// of the packet, it contains the acknowledgement challenge.
159    pub fn acknowledgement_challenge_or_hint(&self) -> HalfKeyChallenge {
160        HalfKeyChallenge::new(&self.0[EthereumChallenge::SIZE..])
161    }
162}
163
164impl std::fmt::Debug for ProofOfRelayString {
165    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
166        f.debug_tuple("ProofOfRelayString")
167            .field(&hex::encode(&self.0[0..EthereumChallenge::SIZE]))
168            .field(&hex::encode(&self.0[EthereumChallenge::SIZE..]))
169            .finish()
170    }
171}
172
173impl TryFrom<&[u8]> for ProofOfRelayString {
174    type Error = GeneralError;
175
176    fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
177        value
178            .try_into()
179            .map(Self)
180            .map_err(|_| GeneralError::ParseError("ProofOfRelayString".into()))
181    }
182}
183
184impl AsRef<[u8]> for ProofOfRelayString {
185    fn as_ref(&self) -> &[u8] {
186        &self.0
187    }
188}
189impl BytesRepresentable for ProofOfRelayString {
190    const SIZE: usize = EthereumChallenge::SIZE + HalfKeyChallenge::SIZE;
191}
192
193/// Derivable challenge which contains the key share of the relayer as well as the secret that was used
194/// to create it and the challenge for the next relayer.
195#[derive(Clone)]
196pub struct ProofOfRelayOutput {
197    pub own_key: HalfKey,
198    pub next_ticket_challenge: EthereumChallenge,
199    pub ack_challenge: HalfKeyChallenge,
200}
201
202/// Verifies that an incoming packet contains all values that are necessary to reconstruct the response to redeem the
203/// incentive for relaying the packet.
204///
205/// # Arguments
206/// * `secret` shared secret with the creator of the packet
207/// * `pors` `ProofOfRelayString` as included within the packet
208/// * `challenge` the ticket challenge of the incoming ticket
209pub fn pre_verify(
210    secret: &SharedSecret,
211    pors: &ProofOfRelayString,
212    challenge: &EthereumChallenge,
213) -> Result<ProofOfRelayOutput> {
214    let own_key = derive_own_key_share(secret);
215    let own_share = own_key.to_challenge();
216
217    if Challenge::from_hint_and_share(&own_share, &pors.acknowledgement_challenge_or_hint())?
218        .to_ethereum_challenge()
219        .eq(challenge)
220    {
221        Ok(ProofOfRelayOutput {
222            next_ticket_challenge: pors.next_ticket_challenge(),
223            ack_challenge: pors.acknowledgement_challenge_or_hint(),
224            own_key,
225        })
226    } else {
227        Err(PacketError::PoRVerificationError)
228    }
229}
230
231/// Helper function which generates proof of relay for the given path.
232pub fn generate_proof_of_relay(secrets: &[SharedSecret]) -> Result<(Vec<ProofOfRelayString>, ProofOfRelayValues)> {
233    let mut last_ack_key_share = None;
234    let mut por_strings = Vec::with_capacity(secrets.len());
235    let mut por_values = None;
236
237    for i in 0..secrets.len() {
238        let hint = last_ack_key_share
239            .unwrap_or_else(|| {
240                derive_ack_key_share(&secrets[i]) // s0_ack
241            })
242            .to_challenge();
243
244        let s1 = derive_own_key_share(&secrets[i]); // s1_own
245        let s2 = derive_ack_key_share(secrets.get(i + 1).unwrap_or(&SharedSecret::random()));
246
247        let next_ticket_challenge = Response::from_half_keys(&s1, &s2)? // (s1_own + s2_ack) * G
248            .to_challenge()
249            .to_ethereum_challenge();
250
251        if i > 0 {
252            por_strings.push(ProofOfRelayString::new(&next_ticket_challenge, &hint));
253        } else {
254            por_values = Some(ProofOfRelayValues::new(
255                secrets.len() as u8,
256                &hint,
257                &next_ticket_challenge,
258            ));
259        }
260        last_ack_key_share = Some(s2);
261    }
262
263    Ok((
264        por_strings,
265        por_values.ok_or(PacketError::LogicError("no shared secrets".into()))?,
266    ))
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    impl ProofOfRelayValues {
274        fn create(
275            secret_b: &SharedSecret,
276            secret_c: Option<&SharedSecret>,
277            chain_length: u8,
278        ) -> hopr_crypto_types::errors::Result<(Self, HalfKey)> {
279            let s0 = derive_own_key_share(secret_b);
280            let s1 = derive_ack_key_share(secret_c.unwrap_or(&SharedSecret::random()));
281
282            let ack_challenge = derive_ack_key_share(secret_b).to_challenge();
283            let ticket_challenge = Response::from_half_keys(&s0, &s1)?
284                .to_challenge()
285                .to_ethereum_challenge();
286
287            Ok((Self::new(chain_length, &ack_challenge, &ticket_challenge), s0))
288        }
289    }
290
291    impl ProofOfRelayString {
292        /// Creates an instance from the shared secrets with node+2 and node+3
293        fn create(secret_c: &SharedSecret, secret_d: Option<&SharedSecret>) -> hopr_crypto_types::errors::Result<Self> {
294            let s0 = derive_ack_key_share(secret_c); // s0_ack
295            let s1 = derive_own_key_share(secret_c); // s1_own
296            let s2 = derive_ack_key_share(secret_d.unwrap_or(&SharedSecret::random())); // s2_ack
297
298            let next_ticket_challenge = Response::from_half_keys(&s1, &s2)? // (s1_own + s2_ack) * G
299                .to_challenge()
300                .to_ethereum_challenge();
301
302            let hint = s0.to_challenge();
303
304            Ok(Self::new(&next_ticket_challenge, &hint))
305        }
306
307        /// Generates Proof of Relay challenges from the shared secrets of the
308        /// outgoing packet.
309        fn from_shared_secrets(secrets: &[SharedSecret]) -> hopr_crypto_types::errors::Result<Vec<ProofOfRelayString>> {
310            (1..secrets.len())
311                .map(|i| ProofOfRelayString::create(&secrets[i], secrets.get(i + 1)))
312                .collect()
313        }
314    }
315
316    /// Checks if the given acknowledgement solves the given challenge.
317    fn validate_por_half_keys(ethereum_challenge: &EthereumChallenge, own_key: &HalfKey, ack: &HalfKey) -> bool {
318        Response::from_half_keys(own_key, ack)
319            .map(|response| validate_por_response(ethereum_challenge, &response))
320            .unwrap_or(false)
321    }
322
323    /// Checks if the given response solves the given challenge.
324    fn validate_por_response(ethereum_challenge: &EthereumChallenge, response: &Response) -> bool {
325        response.to_challenge().to_ethereum_challenge().eq(ethereum_challenge)
326    }
327
328    /// Checks if the given acknowledgement solves the given challenge.
329    fn validate_por_hint(ethereum_challenge: &EthereumChallenge, own_share: &HalfKeyChallenge, ack: &HalfKey) -> bool {
330        Challenge::from_own_share_and_half_key(own_share, ack)
331            .map(|c| c.to_ethereum_challenge().eq(ethereum_challenge))
332            .unwrap_or(false)
333    }
334
335    #[test]
336    fn test_generate_proof_of_relay() -> anyhow::Result<()> {
337        for hops in 0..=3 {
338            let secrets = (0..=hops).map(|_| SharedSecret::random()).collect::<Vec<_>>();
339
340            let por_strings = ProofOfRelayString::from_shared_secrets(&secrets)?;
341            let por_values = ProofOfRelayValues::create(&secrets[0], secrets.get(1), secrets.len() as u8)?.0;
342
343            let (gen_por_strings, gen_por_values) = generate_proof_of_relay(&secrets)?;
344
345            // The ticket challenge is randomly generated for 0-hop, so cannot compare them
346            if hops > 0 {
347                assert_eq!(por_values, gen_por_values);
348            }
349
350            assert_eq!(por_strings.len(), gen_por_strings.len());
351
352            for i in 0..por_strings.len() {
353                assert_eq!(
354                    por_strings[i].acknowledgement_challenge_or_hint(),
355                    gen_por_strings[i].acknowledgement_challenge_or_hint()
356                );
357
358                // The ticket challenge is randomly generated, so cannot compare them
359                if i != por_strings.len() - 1 {
360                    assert_eq!(
361                        por_strings[i].next_ticket_challenge(),
362                        gen_por_strings[i].next_ticket_challenge()
363                    );
364                }
365            }
366        }
367
368        Ok(())
369    }
370
371    #[test]
372    fn test_por_preverify_validate() -> anyhow::Result<()> {
373        const AMOUNT: usize = 4;
374
375        let secrets = (0..AMOUNT).map(|_| SharedSecret::random()).collect::<Vec<_>>();
376
377        // Generated challenge
378        let first_challenge = ProofOfRelayValues::create(&secrets[0], Some(&secrets[1]), secrets.len() as u8)?.0;
379
380        // For the first relayer
381        let first_por_string = ProofOfRelayString::create(&secrets[1], Some(&secrets[2]))?;
382
383        // For the second relayer
384        let second_por_string = ProofOfRelayString::create(&secrets[2], Some(&secrets[3]))?;
385
386        // Computation result of the first relayer before receiving an acknowledgement from the second relayer
387        let first_challenge_eth = first_challenge.ticket_challenge();
388        let first_result = pre_verify(&secrets[0], &first_por_string, &first_challenge_eth)
389            .expect("First challenge must be plausible");
390
391        let expected_hkc = derive_ack_key_share(&secrets[1]).to_challenge();
392        assert_eq!(expected_hkc, first_result.ack_challenge);
393
394        // Simulates the transformation done by the first relayer
395        let expected_pors = ProofOfRelayString::try_from(first_por_string.as_ref())?;
396        assert_eq!(
397            expected_pors.next_ticket_challenge(),
398            first_result.next_ticket_challenge,
399            "Forward logic must extract correct challenge for the next downstream node"
400        );
401
402        // Computes the cryptographic material that is part of the acknowledgement
403        let first_ack = derive_ack_key_share(&secrets[1]);
404        assert!(
405            validate_por_half_keys(&first_challenge.ticket_challenge(), &first_result.own_key, &first_ack),
406            "Acknowledgement must solve the challenge"
407        );
408
409        // Simulates the transformation as done by the second relayer
410        let first_result_challenge_eth = first_result.next_ticket_challenge;
411        let second_result = pre_verify(&secrets[1], &second_por_string, &first_result_challenge_eth)
412            .expect("Second challenge must be plausible");
413
414        let second_ack = derive_ack_key_share(&secrets[2]);
415        assert!(
416            validate_por_half_keys(&first_result.next_ticket_challenge, &second_result.own_key, &second_ack),
417            "Second acknowledgement must solve the challenge"
418        );
419
420        assert!(
421            validate_por_hint(
422                &first_result.next_ticket_challenge,
423                &second_result.own_key.to_challenge(),
424                &second_ack
425            ),
426            "Second acknowledgement must solve the challenge"
427        );
428
429        Ok(())
430    }
431
432    #[test]
433    fn test_challenge_and_response_solving() -> anyhow::Result<()> {
434        const AMOUNT: usize = 2;
435        let secrets = (0..AMOUNT).map(|_| SharedSecret::random()).collect::<Vec<_>>();
436
437        let (first_challenge, own_key) =
438            ProofOfRelayValues::create(&secrets[0], Some(&secrets[1]), secrets.len() as u8)?;
439        let ack = derive_ack_key_share(&secrets[1]);
440
441        assert!(
442            validate_por_half_keys(&first_challenge.ticket_challenge(), &own_key, &ack),
443            "Challenge must be solved"
444        );
445
446        assert!(
447            validate_por_response(
448                &first_challenge.ticket_challenge(),
449                &Response::from_half_keys(&own_key, &ack)?
450            ),
451            "Returned response must solve the challenge"
452        );
453
454        Ok(())
455    }
456}