1use serde::{Deserialize, Serialize};
10
11use super::serde_helpers::{hex_bytes, hex_bytes_vec};
12use super::wire_types::components::{SWF_MAX_DURATION_FACTOR, SWF_MIN_DURATION_FACTOR};
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25pub struct VdfProofRfc {
26 #[serde(rename = "1", with = "hex_bytes")]
27 pub challenge: [u8; 32],
28
29 #[serde(rename = "2", with = "hex_bytes")]
31 pub output: [u8; 64],
32
33 #[serde(rename = "3")]
34 pub iterations: u64,
35
36 #[serde(rename = "4")]
37 pub duration_ms: u64,
38
39 #[serde(rename = "5")]
40 pub calibration: CalibrationAttestation,
41}
42
43impl VdfProofRfc {
44 pub fn new(
46 challenge: [u8; 32],
47 output: [u8; 64],
48 iterations: u64,
49 duration_ms: u64,
50 calibration: CalibrationAttestation,
51 ) -> Self {
52 Self {
53 challenge,
54 output,
55 iterations,
56 duration_ms,
57 calibration,
58 }
59 }
60
61 pub fn minimum_elapsed_ms(&self) -> u64 {
63 if self.calibration.iterations_per_second > 0 {
65 self.iterations
66 .saturating_mul(1000)
67 .checked_div(self.calibration.iterations_per_second)
68 .unwrap_or(self.duration_ms)
69 } else {
70 self.duration_ms
71 }
72 }
73
74 pub fn is_duration_consistent(&self) -> bool {
76 let minimum = self.minimum_elapsed_ms();
77 let threshold = minimum.saturating_sub(minimum / 20);
79 self.duration_ms >= threshold
80 }
81
82 pub fn is_duration_within_spec_bounds(&self) -> bool {
84 let expected = self.minimum_elapsed_ms();
85 if expected == 0 || self.duration_ms == 0 {
86 return false;
87 }
88 let ratio = self.duration_ms as f64 / expected as f64;
89 (SWF_MIN_DURATION_FACTOR..=SWF_MAX_DURATION_FACTOR).contains(&ratio)
90 }
91
92 pub fn iterations_per_ms(&self) -> f64 {
94 if self.duration_ms > 0 {
95 self.iterations as f64 / self.duration_ms as f64
96 } else {
97 0.0
98 }
99 }
100
101 pub fn validate(&self) -> Vec<String> {
103 let mut errors = Vec::new();
104
105 if self.challenge == [0u8; 32] {
106 errors.push("challenge must be non-zero".to_string());
107 }
108
109 if self.output == [0u8; 64] {
110 errors.push("output must be non-zero".to_string());
111 }
112
113 if self.iterations == 0 {
114 errors.push("iterations must be non-zero".to_string());
115 }
116
117 if self.duration_ms == 0 {
118 errors.push("duration_ms must be non-zero".to_string());
119 }
120
121 errors.extend(self.calibration.validate_structure());
122
123 if self.calibration.iterations_per_second > 0 && self.iterations > 0 && self.duration_ms > 0
124 {
125 if !self.is_duration_consistent() {
126 errors.push(format!(
127 "duration_ms ({}) is inconsistent with expected minimum ({} ms) based on calibration",
128 self.duration_ms,
129 self.minimum_elapsed_ms()
130 ));
131 }
132 if !self.is_duration_within_spec_bounds() {
133 let expected = self.minimum_elapsed_ms();
134 let ratio = self.duration_ms as f64 / expected as f64;
135 errors.push(format!(
136 "duration ratio {ratio:.2}x outside spec bounds [{SWF_MIN_DURATION_FACTOR}x, {SWF_MAX_DURATION_FACTOR}x]",
137 ));
138 }
139 }
140
141 errors
142 }
143
144 pub fn is_valid(&self) -> bool {
146 self.validate().is_empty()
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
161pub struct CalibrationAttestation {
162 #[serde(rename = "1")]
163 pub iterations_per_second: u64,
164
165 #[serde(rename = "2")]
167 pub hardware_class: String,
168
169 #[serde(rename = "3", with = "hex_bytes_vec")]
170 pub calibration_signature: Vec<u8>,
171
172 #[serde(rename = "4")]
173 pub timestamp: u64,
174
175 #[serde(rename = "5", skip_serializing_if = "Option::is_none")]
176 pub calibration_authority: Option<String>,
177}
178
179impl CalibrationAttestation {
180 pub fn new(
182 iterations_per_second: u64,
183 hardware_class: String,
184 calibration_signature: Vec<u8>,
185 timestamp: u64,
186 ) -> Self {
187 Self {
188 iterations_per_second,
189 hardware_class,
190 calibration_signature,
191 timestamp,
192 calibration_authority: None,
193 }
194 }
195
196 pub fn with_authority(
198 iterations_per_second: u64,
199 hardware_class: String,
200 calibration_signature: Vec<u8>,
201 timestamp: u64,
202 authority: String,
203 ) -> Self {
204 Self {
205 iterations_per_second,
206 hardware_class,
207 calibration_signature,
208 timestamp,
209 calibration_authority: Some(authority),
210 }
211 }
212
213 pub fn age_seconds(&self, current_time: u64) -> u64 {
215 current_time.saturating_sub(self.timestamp)
216 }
217
218 pub fn is_fresh(&self, current_time: u64) -> bool {
220 self.age_seconds(current_time) < 86400
221 }
222
223 pub fn validate_structure(&self) -> Vec<String> {
225 let mut errors = Vec::new();
226
227 if self.iterations_per_second == 0 {
228 errors.push("calibration.iterations_per_second must be non-zero".to_string());
229 }
230
231 if self.hardware_class.is_empty() {
232 errors.push("calibration.hardware_class must be non-empty".to_string());
233 }
234
235 if self.calibration_signature.is_empty() {
236 errors.push("calibration.calibration_signature must be non-empty".to_string());
237 }
238
239 if self.timestamp == 0 {
240 errors.push("calibration.timestamp must be non-zero".to_string());
241 }
242
243 errors
244 }
245
246 pub fn is_valid(&self) -> bool {
248 self.validate_structure().is_empty()
249 }
250}
251
252#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
254#[serde(rename_all = "snake_case")]
255#[derive(Default)]
256pub enum VdfAlgorithm {
257 #[default]
259 Wesolowski,
260 Pietrzak,
262 Rsa2048,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
268pub struct VdfProofExtended {
269 pub proof: VdfProofRfc,
270 pub algorithm: VdfAlgorithm,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub checkpoints: Option<Vec<VdfCheckpoint>>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
277pub struct VdfCheckpoint {
278 pub iteration: u64,
279 #[serde(with = "hex_bytes")]
280 pub value: [u8; 64],
281 pub elapsed_ms: u64,
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_vdf_proof_creation() {
290 let calibration = CalibrationAttestation::new(
291 1_000_000,
292 "desktop-x86_64".to_string(),
293 vec![0u8; 64],
294 1700000000,
295 );
296
297 let proof = VdfProofRfc::new([1u8; 32], [2u8; 64], 1_000_000, 1000, calibration);
298
299 assert_eq!(proof.iterations, 1_000_000);
300 assert_eq!(proof.duration_ms, 1000);
301 }
302
303 #[test]
304 fn test_minimum_elapsed_calculation() {
305 let calibration = CalibrationAttestation::new(1_000_000, "test".to_string(), vec![], 0);
306
307 let proof = VdfProofRfc::new([0u8; 32], [0u8; 64], 2_000_000, 2500, calibration);
308
309 assert_eq!(proof.minimum_elapsed_ms(), 2000);
310 assert!(proof.is_duration_consistent());
311 }
312
313 #[test]
314 fn test_duration_inconsistent_when_too_fast() {
315 let calibration = CalibrationAttestation::new(1_000_000, "test".to_string(), vec![], 0);
316
317 let proof = VdfProofRfc::new(
318 [0u8; 32],
319 [0u8; 64],
320 2_000_000,
321 500, calibration,
323 );
324
325 assert!(!proof.is_duration_consistent());
326 }
327
328 #[test]
329 fn test_calibration_freshness() {
330 let calibration =
331 CalibrationAttestation::new(1_000_000, "test".to_string(), vec![], 1700000000);
332
333 assert!(calibration.is_fresh(1700000000 + 3600));
334 assert!(calibration.is_fresh(1700000000 + 86000));
335 assert!(!calibration.is_fresh(1700000000 + 90000));
336 }
337
338 #[test]
339 fn test_vdf_proof_serialization() {
340 let calibration = CalibrationAttestation::with_authority(
341 500_000,
342 "mobile-arm64".to_string(),
343 vec![0xAB; 32],
344 1700000000,
345 "writerslogic.com".to_string(),
346 );
347
348 let proof = VdfProofRfc::new([0xDE; 32], [0xAD; 64], 500_000, 1000, calibration);
349
350 let json = serde_json::to_string(&proof).expect("JSON serialization failed");
351 assert!(json.contains("\"1\""));
352 assert!(json.contains("\"2\""));
353
354 let decoded: VdfProofRfc =
355 serde_json::from_str(&json).expect("JSON deserialization failed");
356 assert_eq!(decoded, proof);
357 }
358
359 #[test]
360 fn test_iterations_per_ms() {
361 let calibration = CalibrationAttestation::new(1_000_000, "test".to_string(), vec![1u8], 1);
362
363 let proof = VdfProofRfc::new([1u8; 32], [1u8; 64], 1_000_000, 1000, calibration);
364
365 assert!((proof.iterations_per_ms() - 1000.0).abs() < 0.001);
366 }
367
368 #[test]
369 fn test_vdf_proof_validate_valid() {
370 let calibration = CalibrationAttestation::new(
371 1_000_000,
372 "desktop-x86_64".to_string(),
373 vec![0xAB; 64],
374 1700000000,
375 );
376
377 let proof = VdfProofRfc::new([1u8; 32], [2u8; 64], 1_000_000, 1000, calibration);
378
379 assert!(proof.is_valid());
380 assert!(proof.validate().is_empty());
381 }
382
383 #[test]
384 fn test_vdf_proof_validate_zero_challenge() {
385 let calibration =
386 CalibrationAttestation::new(1_000_000, "test".to_string(), vec![1u8], 1700000000);
387
388 let proof = VdfProofRfc::new([0u8; 32], [2u8; 64], 1_000_000, 1000, calibration);
389
390 let errors = proof.validate();
391 assert!(errors
392 .iter()
393 .any(|e| e.contains("challenge must be non-zero")));
394 assert!(!proof.is_valid());
395 }
396
397 #[test]
398 fn test_vdf_proof_validate_zero_output() {
399 let calibration =
400 CalibrationAttestation::new(1_000_000, "test".to_string(), vec![1u8], 1700000000);
401
402 let proof = VdfProofRfc::new([1u8; 32], [0u8; 64], 1_000_000, 1000, calibration);
403
404 let errors = proof.validate();
405 assert!(errors.iter().any(|e| e.contains("output must be non-zero")));
406 assert!(!proof.is_valid());
407 }
408
409 #[test]
410 fn test_vdf_proof_validate_zero_iterations() {
411 let calibration =
412 CalibrationAttestation::new(1_000_000, "test".to_string(), vec![1u8], 1700000000);
413
414 let proof = VdfProofRfc::new([1u8; 32], [2u8; 64], 0, 1000, calibration);
415
416 let errors = proof.validate();
417 assert!(errors
418 .iter()
419 .any(|e| e.contains("iterations must be non-zero")));
420 assert!(!proof.is_valid());
421 }
422
423 #[test]
424 fn test_vdf_proof_validate_zero_duration() {
425 let calibration =
426 CalibrationAttestation::new(1_000_000, "test".to_string(), vec![1u8], 1700000000);
427
428 let proof = VdfProofRfc::new([1u8; 32], [2u8; 64], 1_000_000, 0, calibration);
429
430 let errors = proof.validate();
431 assert!(errors
432 .iter()
433 .any(|e| e.contains("duration_ms must be non-zero")));
434 assert!(!proof.is_valid());
435 }
436
437 #[test]
438 fn test_vdf_proof_validate_inconsistent_duration() {
439 let calibration =
440 CalibrationAttestation::new(1_000_000, "test".to_string(), vec![1u8], 1700000000);
441
442 let proof = VdfProofRfc::new(
443 [1u8; 32],
444 [2u8; 64],
445 2_000_000,
446 500, calibration,
448 );
449
450 let errors = proof.validate();
451 assert!(errors
452 .iter()
453 .any(|e| e.contains("duration_ms") && e.contains("inconsistent")));
454 assert!(!proof.is_valid());
455 }
456
457 #[test]
458 fn test_calibration_validate_valid() {
459 let calibration = CalibrationAttestation::new(
460 1_000_000,
461 "desktop-x86_64".to_string(),
462 vec![0xAB; 64],
463 1700000000,
464 );
465
466 assert!(calibration.is_valid());
467 assert!(calibration.validate_structure().is_empty());
468 }
469
470 #[test]
471 fn test_calibration_validate_zero_iterations_per_second() {
472 let calibration = CalibrationAttestation::new(0, "test".to_string(), vec![1u8], 1700000000);
473
474 let errors = calibration.validate_structure();
475 assert!(errors
476 .iter()
477 .any(|e| e.contains("iterations_per_second must be non-zero")));
478 assert!(!calibration.is_valid());
479 }
480
481 #[test]
482 fn test_calibration_validate_empty_hardware_class() {
483 let calibration =
484 CalibrationAttestation::new(1_000_000, "".to_string(), vec![1u8], 1700000000);
485
486 let errors = calibration.validate_structure();
487 assert!(errors
488 .iter()
489 .any(|e| e.contains("hardware_class must be non-empty")));
490 assert!(!calibration.is_valid());
491 }
492
493 #[test]
494 fn test_calibration_validate_empty_signature() {
495 let calibration =
496 CalibrationAttestation::new(1_000_000, "test".to_string(), vec![], 1700000000);
497
498 let errors = calibration.validate_structure();
499 assert!(errors
500 .iter()
501 .any(|e| e.contains("calibration_signature must be non-empty")));
502 assert!(!calibration.is_valid());
503 }
504
505 #[test]
506 fn test_calibration_validate_zero_timestamp() {
507 let calibration = CalibrationAttestation::new(1_000_000, "test".to_string(), vec![1u8], 0);
508
509 let errors = calibration.validate_structure();
510 assert!(errors
511 .iter()
512 .any(|e| e.contains("timestamp must be non-zero")));
513 assert!(!calibration.is_valid());
514 }
515
516 #[test]
517 fn test_vdf_proof_validate_multiple_errors() {
518 let calibration = CalibrationAttestation::new(0, "".to_string(), vec![], 0);
519
520 let proof = VdfProofRfc::new([0u8; 32], [0u8; 64], 0, 0, calibration);
521
522 let errors = proof.validate();
523 assert!(errors.len() >= 8);
524 assert!(!proof.is_valid());
525 }
526}