phantom_protocol/crypto/
pow.rs1use blake3::Hasher;
2use borsh::{BorshDeserialize, BorshSerialize};
3use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize};
4use subtle::ConstantTimeEq;
5
6pub const MAX_CLIENT_POW_DIFFICULTY: u8 = 24;
13
14pub const MAX_SOLVE_ITERATIONS: u64 = 1u64 << (MAX_CLIENT_POW_DIFFICULTY as u32 + 8);
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum PowError {
23 DifficultyTooHigh { demanded: u8, cap: u8 },
25 Exhausted,
27}
28
29impl core::fmt::Display for PowError {
30 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
31 match self {
32 PowError::DifficultyTooHigh { demanded, cap } => write!(
33 f,
34 "server demanded PoW difficulty {demanded} exceeding client cap {cap}"
35 ),
36 PowError::Exhausted => write!(f, "PoW solve exhausted the iteration bound"),
37 }
38 }
39}
40
41#[derive(BorshSerialize, BorshDeserialize, SerdeSerialize, SerdeDeserialize, Debug, Clone)]
43pub struct PoWChallenge {
44 pub nonce: [u8; 32], pub difficulty: u8, }
47
48#[derive(BorshSerialize, BorshDeserialize, SerdeSerialize, SerdeDeserialize, Debug, Clone)]
50pub struct PoWSolution {
51 pub nonce: [u8; 32],
52 pub solution: u64,
53}
54
55impl PoWChallenge {
56 pub fn new_stateless(difficulty: u8, client_id: &[u8], secret: &[u8; 32]) -> Self {
65 let timestamp = std::time::SystemTime::now()
66 .duration_since(std::time::UNIX_EPOCH)
67 .unwrap_or_default()
68 .as_secs();
69
70 let mut nonce = [0u8; 32];
71 nonce[0..8].copy_from_slice(×tamp.to_le_bytes());
72
73 let mut hasher = Hasher::new_keyed(secret);
75 hasher.update(×tamp.to_le_bytes());
76 hasher.update(client_id);
77 let mac = hasher.finalize();
78
79 nonce[8..32].copy_from_slice(&mac.as_bytes()[0..24]);
80
81 Self { nonce, difficulty }
82 }
83
84 pub fn verify(&self, solution: &PoWSolution, client_id: &[u8], secret: &[u8; 32]) -> bool {
86 if self.nonce != solution.nonce {
88 return false;
89 }
90
91 let timestamp_bytes: [u8; 8] = self.nonce[0..8].try_into().unwrap_or_default();
93 let timestamp = u64::from_le_bytes(timestamp_bytes);
94
95 let mut hasher = Hasher::new_keyed(secret);
97 hasher.update(×tamp_bytes);
98 hasher.update(client_id);
99 let mac = hasher.finalize();
100
101 if !bool::from(self.nonce[8..32].ct_eq(&mac.as_bytes()[0..24])) {
105 return false;
106 }
107
108 let now = std::time::SystemTime::now()
111 .duration_since(std::time::UNIX_EPOCH)
112 .unwrap_or_default()
113 .as_secs();
114
115 if now < timestamp || now > timestamp + 120 {
116 return false; }
118
119 let hash = compute_blake3_hash(&self.nonce, solution.solution);
122
123 check_leading_zeros(&hash, self.difficulty)
125 }
126
127 pub fn solve(&self) -> Result<PoWSolution, PowError> {
130 self.solve_with_bound(MAX_SOLVE_ITERATIONS)
131 }
132
133 pub fn solve_capped(&self, max_difficulty: u8) -> Result<PoWSolution, PowError> {
138 if self.difficulty > max_difficulty {
139 return Err(PowError::DifficultyTooHigh {
140 demanded: self.difficulty,
141 cap: max_difficulty,
142 });
143 }
144 self.solve()
145 }
146
147 fn solve_with_bound(&self, max_iters: u64) -> Result<PoWSolution, PowError> {
153 for solution in 0..max_iters {
154 let hash = compute_blake3_hash(&self.nonce, solution);
155 if check_leading_zeros(&hash, self.difficulty) {
156 return Ok(PoWSolution {
157 nonce: self.nonce,
158 solution,
159 });
160 }
161 }
162 Err(PowError::Exhausted)
163 }
164}
165fn compute_blake3_hash(nonce: &[u8; 32], solution: u64) -> [u8; 32] {
166 let mut hasher = Hasher::new();
167 hasher.update(nonce);
168 hasher.update(&solution.to_le_bytes());
169 *hasher.finalize().as_bytes()
170}
171
172fn check_leading_zeros(hash: &[u8], difficulty: u8) -> bool {
173 let mut zeros = 0;
174 for &byte in hash {
175 if byte == 0 {
176 zeros += 8;
177 } else {
178 zeros += byte.leading_zeros() as u8;
179 break;
180 }
181 }
182 zeros >= difficulty
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_pow_stateless_verify() {
191 let secret = [42u8; 32];
192 let client_id = b"127.0.0.1";
193
194 let challenge = PoWChallenge::new_stateless(8, client_id, &secret);
195 let solution = challenge.solve().expect("difficulty 8 is solvable");
196
197 assert!(challenge.verify(&solution, client_id, &secret));
198 }
199
200 #[test]
201 fn test_pow_invalid_mac() {
202 let secret = [42u8; 32];
203 let client_id = b"127.0.0.1";
204
205 let mut challenge = PoWChallenge::new_stateless(8, client_id, &secret);
206 challenge.nonce[10] ^= 0xFF; let solution = challenge.solve().expect("difficulty 8 is solvable");
209 assert!(!challenge.verify(&solution, client_id, &secret));
210 }
211
212 #[test]
213 fn test_pow_invalid_client() {
214 let secret = [42u8; 32];
215 let client_id = b"127.0.0.1";
216 let other_client = b"192.168.1.1";
217
218 let challenge = PoWChallenge::new_stateless(8, client_id, &secret);
219 let solution = challenge.solve().expect("difficulty 8 is solvable");
220
221 assert!(!challenge.verify(&solution, other_client, &secret));
222 }
223
224 #[test]
228 fn solve_capped_rejects_oversized_difficulty() {
229 let challenge = PoWChallenge {
230 nonce: [7u8; 32],
231 difficulty: 255,
232 };
233 match challenge.solve_capped(MAX_CLIENT_POW_DIFFICULTY) {
234 Err(PowError::DifficultyTooHigh { demanded, cap }) => {
235 assert_eq!(demanded, 255);
236 assert_eq!(cap, MAX_CLIENT_POW_DIFFICULTY);
237 }
238 other => panic!("expected DifficultyTooHigh, got {:?}", other),
239 }
240 }
241
242 #[test]
244 fn solve_capped_accepts_within_cap() {
245 let secret = [9u8; 32];
246 let client_id = b"127.0.0.1";
247 let challenge = PoWChallenge::new_stateless(8, client_id, &secret);
248 assert!(challenge.difficulty <= MAX_CLIENT_POW_DIFFICULTY);
249 let solution = challenge
250 .solve_capped(MAX_CLIENT_POW_DIFFICULTY)
251 .expect("difficulty 8 is solvable");
252 assert!(challenge.verify(&solution, client_id, &secret));
253 }
254
255 #[test]
258 fn solve_is_bounded_and_fails_closed() {
259 let challenge = PoWChallenge {
260 nonce: [3u8; 32],
261 difficulty: 250,
262 };
263 assert!(matches!(
264 challenge.solve_with_bound(1_000),
265 Err(PowError::Exhausted)
266 ));
267 }
268
269 #[test]
274 fn max_client_pow_difficulty_admits_the_server_max() {
275 assert!(MAX_CLIENT_POW_DIFFICULTY >= 20);
276 }
277}