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 {
60 let timestamp = std::time::SystemTime::now()
61 .duration_since(std::time::UNIX_EPOCH)
62 .unwrap_or_default()
63 .as_secs();
64
65 let mut nonce = [0u8; 32];
66 nonce[0..8].copy_from_slice(×tamp.to_le_bytes());
67
68 let mut hasher = Hasher::new_keyed(secret);
70 hasher.update(×tamp.to_le_bytes());
71 hasher.update(client_id);
72 let mac = hasher.finalize();
73
74 nonce[8..32].copy_from_slice(&mac.as_bytes()[0..24]);
75
76 Self { nonce, difficulty }
77 }
78
79 pub fn verify(&self, solution: &PoWSolution, client_id: &[u8], secret: &[u8; 32]) -> bool {
81 if self.nonce != solution.nonce {
83 return false;
84 }
85
86 let timestamp_bytes: [u8; 8] = self.nonce[0..8].try_into().unwrap_or_default();
88 let timestamp = u64::from_le_bytes(timestamp_bytes);
89
90 let mut hasher = Hasher::new_keyed(secret);
92 hasher.update(×tamp_bytes);
93 hasher.update(client_id);
94 let mac = hasher.finalize();
95
96 if !bool::from(self.nonce[8..32].ct_eq(&mac.as_bytes()[0..24])) {
100 return false;
101 }
102
103 let now = std::time::SystemTime::now()
105 .duration_since(std::time::UNIX_EPOCH)
106 .unwrap_or_default()
107 .as_secs();
108
109 if now < timestamp || now > timestamp + 120 {
110 return false; }
112
113 let hash = compute_blake3_hash(&self.nonce, solution.solution);
116
117 check_leading_zeros(&hash, self.difficulty)
119 }
120
121 pub fn solve(&self) -> Result<PoWSolution, PowError> {
124 self.solve_with_bound(MAX_SOLVE_ITERATIONS)
125 }
126
127 pub fn solve_capped(&self, max_difficulty: u8) -> Result<PoWSolution, PowError> {
132 if self.difficulty > max_difficulty {
133 return Err(PowError::DifficultyTooHigh {
134 demanded: self.difficulty,
135 cap: max_difficulty,
136 });
137 }
138 self.solve()
139 }
140
141 fn solve_with_bound(&self, max_iters: u64) -> Result<PoWSolution, PowError> {
147 for solution in 0..max_iters {
148 let hash = compute_blake3_hash(&self.nonce, solution);
149 if check_leading_zeros(&hash, self.difficulty) {
150 return Ok(PoWSolution {
151 nonce: self.nonce,
152 solution,
153 });
154 }
155 }
156 Err(PowError::Exhausted)
157 }
158}
159fn compute_blake3_hash(nonce: &[u8; 32], solution: u64) -> [u8; 32] {
160 let mut hasher = Hasher::new();
161 hasher.update(nonce);
162 hasher.update(&solution.to_le_bytes());
163 *hasher.finalize().as_bytes()
164}
165
166fn check_leading_zeros(hash: &[u8], difficulty: u8) -> bool {
167 let mut zeros = 0;
168 for &byte in hash {
169 if byte == 0 {
170 zeros += 8;
171 } else {
172 zeros += byte.leading_zeros() as u8;
173 break;
174 }
175 }
176 zeros >= difficulty
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_pow_stateless_verify() {
185 let secret = [42u8; 32];
186 let client_id = b"127.0.0.1";
187
188 let challenge = PoWChallenge::new_stateless(8, client_id, &secret);
189 let solution = challenge.solve().expect("difficulty 8 is solvable");
190
191 assert!(challenge.verify(&solution, client_id, &secret));
192 }
193
194 #[test]
195 fn test_pow_invalid_mac() {
196 let secret = [42u8; 32];
197 let client_id = b"127.0.0.1";
198
199 let mut challenge = PoWChallenge::new_stateless(8, client_id, &secret);
200 challenge.nonce[10] ^= 0xFF; let solution = challenge.solve().expect("difficulty 8 is solvable");
203 assert!(!challenge.verify(&solution, client_id, &secret));
204 }
205
206 #[test]
207 fn test_pow_invalid_client() {
208 let secret = [42u8; 32];
209 let client_id = b"127.0.0.1";
210 let other_client = b"192.168.1.1";
211
212 let challenge = PoWChallenge::new_stateless(8, client_id, &secret);
213 let solution = challenge.solve().expect("difficulty 8 is solvable");
214
215 assert!(!challenge.verify(&solution, other_client, &secret));
216 }
217
218 #[test]
222 fn solve_capped_rejects_oversized_difficulty() {
223 let challenge = PoWChallenge {
224 nonce: [7u8; 32],
225 difficulty: 255,
226 };
227 match challenge.solve_capped(MAX_CLIENT_POW_DIFFICULTY) {
228 Err(PowError::DifficultyTooHigh { demanded, cap }) => {
229 assert_eq!(demanded, 255);
230 assert_eq!(cap, MAX_CLIENT_POW_DIFFICULTY);
231 }
232 other => panic!("expected DifficultyTooHigh, got {:?}", other),
233 }
234 }
235
236 #[test]
238 fn solve_capped_accepts_within_cap() {
239 let secret = [9u8; 32];
240 let client_id = b"127.0.0.1";
241 let challenge = PoWChallenge::new_stateless(8, client_id, &secret);
242 assert!(challenge.difficulty <= MAX_CLIENT_POW_DIFFICULTY);
243 let solution = challenge
244 .solve_capped(MAX_CLIENT_POW_DIFFICULTY)
245 .expect("difficulty 8 is solvable");
246 assert!(challenge.verify(&solution, client_id, &secret));
247 }
248
249 #[test]
252 fn solve_is_bounded_and_fails_closed() {
253 let challenge = PoWChallenge {
254 nonce: [3u8; 32],
255 difficulty: 250,
256 };
257 assert!(matches!(
258 challenge.solve_with_bound(1_000),
259 Err(PowError::Exhausted)
260 ));
261 }
262
263 #[test]
268 fn max_client_pow_difficulty_admits_the_server_max() {
269 assert!(MAX_CLIENT_POW_DIFFICULTY >= 20);
270 }
271}