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/// Contains the Proof of Relay challenge for the next downstream node as well as the hint that is used to
95/// verify the challenge that is given to the relayer.
96#[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    /// Challenge that must be printed on the ticket for the next downstream node.
108    pub fn next_ticket_challenge(&self) -> EthereumChallenge {
109        EthereumChallenge(Address::new(&self.0[0..EthereumChallenge::SIZE]))
110    }
111
112    /// Proof of Relay hint value for this node. In case this node is a sender
113    /// of the packet, it contains the acknowledgement challenge.
114    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/// Derivable challenge which contains the key share of the relayer as well as the secret that was used
149/// to create it and the challenge for the next relayer.
150#[derive(Clone)]
151pub struct ProofOfRelayOutput {
152    pub own_key: HalfKey,
153    pub next_ticket_challenge: EthereumChallenge,
154    pub ack_challenge: HalfKeyChallenge,
155}
156
157/// Verifies that an incoming packet contains all values that are necessary to reconstruct the response to redeem the
158/// incentive for relaying the packet.
159///
160/// # Arguments
161/// * `secret` shared secret with the creator of the packet
162/// * `pors` `ProofOfRelayString` as included within the packet
163/// * `challenge` the ticket challenge of the incoming ticket
164#[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
187/// Contains Proof of Relay values and a solution to the first acknowledgement challenge.
188///
189/// This is useful when additional pre-conditioning is needed on the acknowledgement
190/// sent by the first relayer, such as in PIX.
191pub type ProofOfRelayValuesWithSolution = (ProofOfRelayValues, HalfKey);
192
193/// Helper function that generates proof of relay for the given path.
194pub 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]); // s0_ack
206
207    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]); // s1_own
212            let s2 = derive_ack_key_share(next_secret); // s2_ack
213
214            last_ack_key_share = Some(s2);
215
216            Response::from_half_keys(&s1, &s2)? // (s1_own + s2_ack) * G
217                .to_challenge()?
218                .to_ethereum_challenge()
219        } else {
220            // NOTE: we do not generate a random ack_key_share to create the challenge for performance reasons
221            // This means for 0-hop packets, the solution to the Proof of Relay is unknown, because
222            // we do not even try to solve it in such case.
223            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            // Cannot panic due to the first check
241            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        /// Creates an instance from the shared secrets with node+2 and node+3
272        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); // s0_ack
277            let s1 = derive_own_key_share(secret_c); // s1_own
278            let s2 = derive_ack_key_share(secret_d.unwrap_or(&SharedSecret::random())); // s2_ack
279
280            let next_ticket_challenge = Response::from_half_keys(&s1, &s2)? // (s1_own + s2_ack) * G
281                .to_challenge()?
282                .to_ethereum_challenge();
283
284            let hint = s0.to_challenge()?;
285            Ok(Self::new(&next_ticket_challenge, &hint))
286        }
287
288        /// Generates Proof of Relay challenges from the shared secrets of the
289        /// outgoing packet.
290        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    /// Checks if the given acknowledgement solves the given challenge.
300    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    /// Checks if the given response solves the given challenge.
307    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    /// Checks if the given acknowledgement solves the given challenge.
314    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            // The solution to the first Proof of Relay must be the acknowledgement key share derived from the first
331            // shared secret.
332            assert_eq!(gen_por_sol, derive_ack_key_share(&secrets[0]));
333
334            // The chain length should be the number of nodes in the path (hops + 1).
335
336            assert_eq!(gen_por_values.chain_length(), (hops + 1) as u8);
337
338            // The acknowledgement challenge in the PoR values must be solved by the generated solution.
339            assert_eq!(gen_por_values.acknowledgement_challenge(), gen_por_sol.to_challenge()?);
340
341            // The ticket challenge is randomly generated for 0-hop, so cannot compare them
342            if hops > 0 {
343                // For paths with at least one hop, the generated PoR values should match the expected values.
344                assert_eq!(por_values, gen_por_values);
345
346                // The ticket challenge of the first node must be solved by its own key share and the next node's
347                // acknowledgement key share.
348                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                // pre_verify should correctly transition from the current node's challenge to the next node's PoR data.
355                let res = pre_verify(&secrets[0], &gen_por_strings[0], &gen_por_values.ticket_challenge())?;
356                // The extracted next ticket challenge must match the one in the PoR string.
357                assert_eq!(res.next_ticket_challenge, gen_por_strings[0].next_ticket_challenge());
358
359                // The extracted acknowledgement challenge must match the hint/challenge in the PoR string.
360                assert_eq!(
361                    res.ack_challenge,
362                    gen_por_strings[0].acknowledgement_challenge_or_hint()
363                );
364
365                // The derived own key must match the expected one for the first hop.
366                assert_eq!(res.own_key, derive_own_key_share(&secrets[0]));
367            }
368
369            // The number of Proof of Relay strings should match the number of hops.
370            assert_eq!(por_strings.len(), gen_por_strings.len());
371
372            for i in 0..por_strings.len() {
373                // Each PoR string's hint should match the expected one.
374                assert_eq!(
375                    por_strings[i].acknowledgement_challenge_or_hint(),
376                    gen_por_strings[i].acknowledgement_challenge_or_hint()
377                );
378                // Each hint must be the challenge form of the corresponding node's acknowledgement key share.
379                assert_eq!(
380                    gen_por_strings[i].acknowledgement_challenge_or_hint(),
381                    derive_ack_key_share(&secrets[i + 1]).to_challenge()?
382                );
383
384                // The ticket challenge is randomly generated, so cannot compare them
385                if i != por_strings.len() - 1 {
386                    // The generated next ticket challenge should match the expected one.
387                    assert_eq!(
388                        por_strings[i].next_ticket_challenge(),
389                        gen_por_strings[i].next_ticket_challenge()
390                    );
391
392                    // Each hop's ticket challenge must be solved by its own key share and the next hop's
393                    // acknowledgement key share.
394                    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                    // pre_verify should work for all hops, correctly extracting the next challenge and verifying the
401                    // current one.
402                    let res = pre_verify(
403                        &secrets[i + 1],
404                        &gen_por_strings[i + 1],
405                        &gen_por_strings[i].next_ticket_challenge(),
406                    )?;
407                    // Verifies that the forwarded challenge matches the next one in the chain.
408                    assert_eq!(
409                        res.next_ticket_challenge,
410                        gen_por_strings[i + 1].next_ticket_challenge()
411                    );
412                    // Verifies that the extracted acknowledgement challenge matches the next hint.
413                    assert_eq!(
414                        res.ack_challenge,
415                        gen_por_strings[i + 1].acknowledgement_challenge_or_hint()
416                    );
417                    // Verifies that the correct own key is derived for each intermediate hop.
418                    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        // Generated challenge
433        let first_challenge = ProofOfRelayValues::create(&secrets[0], Some(&secrets[1]), secrets.len() as u8)?.0;
434
435        // For the first relayer
436        let first_por_string = ProofOfRelayString::create(&secrets[1], Some(&secrets[2]))?;
437
438        // For the second relayer
439        let second_por_string = ProofOfRelayString::create(&secrets[2], Some(&secrets[3]))?;
440
441        // Computation result of the first relayer before receiving an acknowledgement from the second relayer
442        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        // Simulates the transformation done by the first relayer
450        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        // Computes the cryptographic material that is part of the acknowledgement
458        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        // Simulates the transformation as done by the second relayer
465        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}