Skip to main content

hopr_crypto_packet/
por.rs

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