hopr_crypto_packet/
por.rs

1use hopr_crypto_sphinx::{
2    derivation::{derive_ack_key_share, derive_own_key_share},
3    shared_keys::SharedSecret,
4};
5use hopr_crypto_types::types::{Challenge, CurvePoint, HalfKey, HalfKeyChallenge, PublicKey, Response};
6use hopr_primitive_types::prelude::*;
7use tracing::error;
8
9use crate::errors::{PacketError, Result};
10
11/// Proof of Relay secret length is twice the size of secp256k1 public key
12pub const POR_SECRET_LENGTH: usize = 2 * PublicKey::SIZE_COMPRESSED;
13
14/// Type that contains the challenge for the first ticket sent to the first relayer.
15#[derive(Clone)]
16pub struct ProofOfRelayValues {
17    pub ack_challenge: HalfKeyChallenge,
18    pub ticket_challenge: Challenge,
19    pub own_key: HalfKey,
20}
21
22impl ProofOfRelayValues {
23    /// Takes the secrets which the first and the second relayer are able to derive from the packet header
24    /// and computes the challenge for the first ticket.
25    pub fn new(secret_b: &SharedSecret, secret_c: Option<&SharedSecret>) -> Self {
26        let s0 = derive_own_key_share(secret_b);
27        let s1 = derive_ack_key_share(secret_c.unwrap_or(&SharedSecret::random()));
28
29        Self {
30            // ack_challenge = s0_ack * G
31            ack_challenge: derive_ack_key_share(secret_b).to_challenge(),
32            // ticket challenge (s0_own + s1_ack) * G
33            ticket_challenge: Response::from_half_keys(&s0, &s1)
34                .expect("failed to derive response")
35                .to_challenge(),
36            own_key: s0,
37        }
38    }
39}
40
41/// Contains the Proof of Relay challenge for the next downstream node as well as the hint that is used to
42/// verify the challenge that is given to the relayer.
43#[derive(Clone)]
44pub struct ProofOfRelayString {
45    pub next_ticket_challenge: Challenge,
46    pub hint: HalfKeyChallenge,
47}
48
49impl ProofOfRelayString {
50    /// Creates an instance from the shared secrets with node+2 and node+3
51    pub fn new(secret_c: &SharedSecret, secret_d: Option<&SharedSecret>) -> Self {
52        let s0 = derive_ack_key_share(secret_c); // s0_ack
53        let s1 = derive_own_key_share(secret_c); // s1_own
54        let s2 = derive_ack_key_share(secret_d.unwrap_or(&SharedSecret::random())); // s2_ack
55
56        Self {
57            next_ticket_challenge: Response::from_half_keys(&s1, &s2) // (s1_own + s2_ack) * G
58                .expect("failed to derive response")
59                .to_challenge(),
60            hint: s0.to_challenge(), // s0_ack * G
61        }
62    }
63
64    /// Generates Proof of Relay challenges from the shared secrets of the
65    /// outgoing packet.
66    pub fn from_shared_secrets(secrets: &[SharedSecret]) -> Vec<[u8; ProofOfRelayString::SIZE]> {
67        (1..secrets.len())
68            .map(|i| ProofOfRelayString::new(&secrets[i], secrets.get(i + 1)).into())
69            .collect::<Vec<_>>()
70    }
71}
72
73impl TryFrom<&[u8]> for ProofOfRelayString {
74    type Error = GeneralError;
75
76    fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
77        if value.len() == POR_SECRET_LENGTH {
78            let (next_ticket_challenge, hint) = value.split_at(POR_SECRET_LENGTH / 2);
79            Ok(Self {
80                next_ticket_challenge: CurvePoint::try_from(next_ticket_challenge)?.into(),
81                hint: HalfKeyChallenge::new(hint),
82            })
83        } else {
84            Err(GeneralError::ParseError("ProofOfRelayString".into()))
85        }
86    }
87}
88
89impl From<ProofOfRelayString> for [u8; PROOF_OF_RELAY_STRING_LEN] {
90    fn from(value: ProofOfRelayString) -> Self {
91        let mut ret = [0u8; PROOF_OF_RELAY_STRING_LEN];
92        ret[0..Challenge::SIZE].copy_from_slice(value.next_ticket_challenge.as_ref());
93        ret[Challenge::SIZE..].copy_from_slice(value.hint.as_ref());
94        ret
95    }
96}
97
98const PROOF_OF_RELAY_STRING_LEN: usize = Challenge::SIZE + HalfKeyChallenge::SIZE;
99impl BytesEncodable<PROOF_OF_RELAY_STRING_LEN> for ProofOfRelayString {}
100
101/// Derivable challenge which contains the key share of the relayer as well as the secret that was used
102/// to create it and the challenge for the next relayer.
103#[derive(Clone)]
104pub struct ProofOfRelayOutput {
105    pub own_key: HalfKey,
106    pub own_share: HalfKeyChallenge,
107    pub next_ticket_challenge: Challenge,
108    pub ack_challenge: HalfKeyChallenge,
109}
110
111/// Verifies that an incoming packet contains all values that are necessary to reconstruct the response to redeem the
112/// incentive for relaying the packet.
113/// # Arguments
114/// * `secret` shared secret with the creator of the packet
115/// * `por_bytes` serialized `ProofOfRelayString` as included within the packet
116/// * `challenge` ticket challenge of the incoming ticket
117pub fn pre_verify(
118    secret: &SharedSecret,
119    por_bytes: &[u8],
120    challenge: &EthereumChallenge,
121) -> Result<ProofOfRelayOutput> {
122    assert_eq!(POR_SECRET_LENGTH, por_bytes.len(), "invalid por bytes length");
123
124    let pors = ProofOfRelayString::try_from(por_bytes)?;
125
126    let own_key = derive_own_key_share(secret);
127    let own_share = own_key.to_challenge();
128
129    if Challenge::from_hint_and_share(&own_share, &pors.hint)?
130        .to_ethereum_challenge()
131        .eq(challenge)
132    {
133        Ok(ProofOfRelayOutput {
134            next_ticket_challenge: pors.next_ticket_challenge,
135            ack_challenge: pors.hint,
136            own_key,
137            own_share,
138        })
139    } else {
140        Err(PacketError::PoRVerificationError)
141    }
142}
143
144/// Checks if the given acknowledgement solves the given challenge.
145pub fn validate_por_half_keys(ethereum_challenge: &EthereumChallenge, own_key: &HalfKey, ack: &HalfKey) -> bool {
146    Response::from_half_keys(own_key, ack)
147        .map(|response| validate_por_response(ethereum_challenge, &response))
148        .unwrap_or_else(|e| {
149            error!(error = %e, "failed to validate por half keys");
150            false
151        })
152}
153
154/// Checks if the given response solves the given challenge.
155pub fn validate_por_response(ethereum_challenge: &EthereumChallenge, response: &Response) -> bool {
156    response.to_challenge().to_ethereum_challenge().eq(ethereum_challenge)
157}
158
159/// Checks if the given acknowledgement solves the given challenge.
160pub fn validate_por_hint(ethereum_challenge: &EthereumChallenge, own_share: &HalfKeyChallenge, ack: &HalfKey) -> bool {
161    Challenge::from_own_share_and_half_key(own_share, ack)
162        .map(|c| c.to_ethereum_challenge().eq(ethereum_challenge))
163        .unwrap_or_else(|e| {
164            error!(error = %e,"failed to validate por hint");
165            false
166        })
167}
168
169#[cfg(test)]
170mod tests {
171    use crate::por::{
172        pre_verify, validate_por_half_keys, validate_por_hint, validate_por_response, ProofOfRelayString,
173        ProofOfRelayValues,
174    };
175    use hopr_crypto_sphinx::derivation::derive_ack_key_share;
176    use hopr_crypto_sphinx::shared_keys::SharedSecret;
177    use hopr_crypto_types::types::Response;
178    use hopr_primitive_types::traits::BytesEncodable;
179
180    #[test]
181    fn test_por_preverify_validate() -> anyhow::Result<()> {
182        const AMOUNT: usize = 4;
183
184        let secrets = (0..AMOUNT).map(|_| SharedSecret::random()).collect::<Vec<_>>();
185
186        // Generated challenge
187        let first_challenge = ProofOfRelayValues::new(&secrets[0], Some(&secrets[1]));
188
189        // For the first relayer
190        let first_por_string = ProofOfRelayString::new(&secrets[1], Some(&secrets[2])).into_encoded();
191
192        // For the second relayer
193        let second_por_string = ProofOfRelayString::new(&secrets[2], Some(&secrets[3]));
194
195        // Computation result of the first relayer before receiving an acknowledgement from the second relayer
196        let first_challenge_eth = first_challenge.ticket_challenge.to_ethereum_challenge();
197        let first_result = pre_verify(&secrets[0], &first_por_string, &first_challenge_eth)
198            .expect("First challenge must be plausible");
199
200        let expected_hkc = derive_ack_key_share(&secrets[1]).to_challenge();
201        assert_eq!(expected_hkc, first_result.ack_challenge);
202
203        // Simulates the transformation done by the first relayer
204        let expected_pors = ProofOfRelayString::try_from(first_por_string.as_ref())?;
205        assert_eq!(
206            expected_pors.next_ticket_challenge, first_result.next_ticket_challenge,
207            "Forward logic must extract correct challenge for the next downstream node"
208        );
209
210        // Computes the cryptographic material that is part of the acknowledgement
211        let first_ack = derive_ack_key_share(&secrets[1]);
212        assert!(
213            validate_por_half_keys(
214                &first_challenge.ticket_challenge.to_ethereum_challenge(),
215                &first_result.own_key,
216                &first_ack
217            ),
218            "Acknowledgement must solve the challenge"
219        );
220
221        // Simulates the transformation as done by the second relayer
222        let first_result_challenge_eth = first_result.next_ticket_challenge.to_ethereum_challenge();
223        let second_result = pre_verify(
224            &secrets[1],
225            &second_por_string.into_encoded(),
226            &first_result_challenge_eth,
227        )
228        .expect("Second challenge must be plausible");
229
230        let second_ack = derive_ack_key_share(&secrets[2]);
231        assert!(
232            validate_por_half_keys(
233                &first_result.next_ticket_challenge.to_ethereum_challenge(),
234                &second_result.own_key,
235                &second_ack
236            ),
237            "Second acknowledgement must solve the challenge"
238        );
239
240        assert!(
241            validate_por_hint(
242                &first_result.next_ticket_challenge.to_ethereum_challenge(),
243                &second_result.own_share,
244                &second_ack
245            ),
246            "Second acknowledgement must solve the challenge"
247        );
248
249        Ok(())
250    }
251
252    #[test]
253    fn test_challenge_and_response_solving() -> anyhow::Result<()> {
254        const AMOUNT: usize = 2;
255        let secrets = (0..AMOUNT).map(|_| SharedSecret::random()).collect::<Vec<_>>();
256
257        let first_challenge = ProofOfRelayValues::new(&secrets[0], Some(&secrets[1]));
258        let ack = derive_ack_key_share(&secrets[1]);
259
260        assert!(
261            validate_por_half_keys(
262                &first_challenge.ticket_challenge.to_ethereum_challenge(),
263                &first_challenge.own_key,
264                &ack
265            ),
266            "Challenge must be solved"
267        );
268
269        assert!(
270            validate_por_response(
271                &first_challenge.ticket_challenge.to_ethereum_challenge(),
272                &Response::from_half_keys(&first_challenge.own_key, &ack)?
273            ),
274            "Returned response must solve the challenge"
275        );
276
277        Ok(())
278    }
279}