hopr_crypto_packet/
por.rs

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
13/// Used in Proof of Relay to derive own half-key (S0)
14/// The function samples a secp256k1 field element using the given `secret` via `sample_field_element`.
15fn 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
19/// Used in Proof of Relay to derive the half-key of for the acknowledgement (S1)
20/// The function samples a secp256k1 field element using the given `secret` via `sample_field_element`.
21pub 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/// Type that contains the challenge for the first ticket sent to the first relayer.
26///
27/// This is the first entry of the entire PoR challenge chain generated for the packet.
28#[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    /// Length of this PoR challenge chain (number of hops + 1).
42    // TODO: needed to know how to price the ticket on the return path, will be fixed in #3765
43    pub fn chain_length(&self) -> u8 {
44        self.0[0]
45    }
46
47    /// Returns the challenge that must be solved once the acknowledgement
48    /// to the packet has been received.
49    ///
50    /// This is the [`ProofOfRelayValues::ticket_challenge`] minus the Hint.
51    pub fn acknowledgement_challenge(&self) -> HalfKeyChallenge {
52        HalfKeyChallenge::new(&self.0[1..1 + HalfKeyChallenge::SIZE])
53    }
54
55    /// Returns the complete challenge that is present on the ticket corresponding to the
56    /// packet.
57    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/// Wraps the [`ProofOfRelayValues`] with some additional information about the sender of the packet,
94/// that is supposed to be passed along with the SURB.
95// TODO: currently 32 bytes are reserved for future use by Shamir's secret sharing scheme.
96#[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        // Share is currently not used but will be used in the future
105        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/// Contains the Proof of Relay challenge for the next downstream node as well as the hint that is used to
137/// verify the challenge that is given to the relayer.
138#[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    /// Challenge that must be printed on the ticket for the next downstream node.
150    pub fn next_ticket_challenge(&self) -> EthereumChallenge {
151        EthereumChallenge(Address::new(&self.0[0..EthereumChallenge::SIZE]))
152    }
153
154    /// Proof of Relay hint value for this node. In case this node is a sender
155    /// of the packet, it contains the acknowledgement challenge.
156    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/// Derivable challenge which contains the key share of the relayer as well as the secret that was used
191/// to create it and the challenge for the next relayer.
192#[derive(Clone)]
193pub struct ProofOfRelayOutput {
194    pub own_key: HalfKey,
195    pub next_ticket_challenge: EthereumChallenge,
196    pub ack_challenge: HalfKeyChallenge,
197}
198
199/// Verifies that an incoming packet contains all values that are necessary to reconstruct the response to redeem the
200/// incentive for relaying the packet.
201///
202/// # Arguments
203/// * `secret` shared secret with the creator of the packet
204/// * `pors` `ProofOfRelayString` as included within the packet
205/// * `challenge` the ticket challenge of the incoming ticket
206#[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
229/// Helper function which generates proof of relay for the given path.
230pub 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]) // s0_ack
239            })
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]); // s1_own
244            let s2 = derive_ack_key_share(next_secret); // s2_ack
245
246            last_ack_key_share = Some(s2);
247
248            Response::from_half_keys(&s1, &s2)? // (s1_own + s2_ack) * G
249                .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        /// Creates an instance from the shared secrets with node+2 and node+3
297        fn create(secret_c: &SharedSecret, secret_d: Option<&SharedSecret>) -> hopr_crypto_types::errors::Result<Self> {
298            let s0 = derive_ack_key_share(secret_c); // s0_ack
299            let s1 = derive_own_key_share(secret_c); // s1_own
300            let s2 = derive_ack_key_share(secret_d.unwrap_or(&SharedSecret::random())); // s2_ack
301
302            let next_ticket_challenge = Response::from_half_keys(&s1, &s2)? // (s1_own + s2_ack) * G
303                .to_challenge()?
304                .to_ethereum_challenge();
305
306            let hint = s0.to_challenge()?;
307            Ok(Self::new(&next_ticket_challenge, &hint))
308        }
309
310        /// Generates Proof of Relay challenges from the shared secrets of the
311        /// outgoing packet.
312        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    /// Checks if the given acknowledgement solves the given challenge.
320    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    /// Checks if the given response solves the given challenge.
327    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    /// Checks if the given acknowledgement solves the given challenge.
334    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            // The ticket challenge is randomly generated for 0-hop, so cannot compare them
351            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                // The ticket challenge is randomly generated, so cannot compare them
364                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        // Generated challenge
383        let first_challenge = ProofOfRelayValues::create(&secrets[0], Some(&secrets[1]), secrets.len() as u8)?.0;
384
385        // For the first relayer
386        let first_por_string = ProofOfRelayString::create(&secrets[1], Some(&secrets[2]))?;
387
388        // For the second relayer
389        let second_por_string = ProofOfRelayString::create(&secrets[2], Some(&secrets[3]))?;
390
391        // Computation result of the first relayer before receiving an acknowledgement from the second relayer
392        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        // Simulates the transformation done by the first relayer
400        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        // Computes the cryptographic material that is part of the acknowledgement
408        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        // Simulates the transformation as done by the second relayer
415        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}