hopr_crypto_packet/
por.rs1use 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
11pub const POR_SECRET_LENGTH: usize = 2 * PublicKey::SIZE_COMPRESSED;
13
14#[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 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: derive_ack_key_share(secret_b).to_challenge(),
32 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#[derive(Clone)]
44pub struct ProofOfRelayString {
45 pub next_ticket_challenge: Challenge,
46 pub hint: HalfKeyChallenge,
47}
48
49impl ProofOfRelayString {
50 pub fn new(secret_c: &SharedSecret, secret_d: Option<&SharedSecret>) -> Self {
52 let s0 = derive_ack_key_share(secret_c); let s1 = derive_own_key_share(secret_c); let s2 = derive_ack_key_share(secret_d.unwrap_or(&SharedSecret::random())); Self {
57 next_ticket_challenge: Response::from_half_keys(&s1, &s2) .expect("failed to derive response")
59 .to_challenge(),
60 hint: s0.to_challenge(), }
62 }
63
64 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#[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
111pub 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
144pub 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
154pub fn validate_por_response(ethereum_challenge: &EthereumChallenge, response: &Response) -> bool {
156 response.to_challenge().to_ethereum_challenge().eq(ethereum_challenge)
157}
158
159pub 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 let first_challenge = ProofOfRelayValues::new(&secrets[0], Some(&secrets[1]));
188
189 let first_por_string = ProofOfRelayString::new(&secrets[1], Some(&secrets[2])).into_encoded();
191
192 let second_por_string = ProofOfRelayString::new(&secrets[2], Some(&secrets[3]));
194
195 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 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 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 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}