1use crate::tools::pow::pow_measure_from_data_hash;
16use crate::tools::pow_required_estimator::PowRequiredEstimator;
17use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
18use crate::tools::types::{Hash, Pow, Salt};
19use futures::future::Either;
20use futures::stream::{FuturesUnordered, StreamExt};
21use log::trace;
22use std::collections::HashMap;
23use std::future::Future;
24use std::pin::Pin;
25use std::sync::{Arc, Mutex};
26use tokio_util::sync::CancellationToken;
27
28pub struct PowJobStatus {
29 pub label: String,
30 pub pow_min: Pow,
31 pub best_pow_so_far: Pow,
32}
33
34type JobId = u64;
35
36struct JobEntry {
37 label: String,
38 pow_min: Pow,
39 best_pow_so_far: Pow,
40}
41
42#[derive(Default)]
43pub struct JobTracker {
44 next_id: JobId,
45 jobs: HashMap<JobId, JobEntry>,
46 last_work_time_millis: i64,
49}
50
51impl JobTracker {
52 pub fn add(&mut self, label: &str, pow_min: Pow) -> JobId {
53 let job_id = self.next_id;
54 self.next_id += 1;
55 self.jobs.insert(job_id, JobEntry { label: label.to_string(), pow_min, best_pow_so_far: Pow(0) });
56 job_id
57 }
58
59 pub fn update(&mut self, job_id: JobId, best_pow_so_far: Pow) {
60 if let Some(entry) = self.jobs.get_mut(&job_id) {
61 entry.best_pow_so_far = best_pow_so_far;
62 }
63 }
64
65 pub fn remove(&mut self, job_id: JobId) {
66 self.jobs.remove(&job_id);
67 }
68
69 pub fn mark_work(&mut self, now_millis: i64) {
71 self.last_work_time_millis = now_millis;
72 }
73
74 pub fn is_busy(&self, now_millis: i64, within_millis: i64) -> bool {
78 !self.jobs.is_empty() || (self.last_work_time_millis != 0 && now_millis - self.last_work_time_millis <= within_millis)
79 }
80
81 pub fn snapshot(&self) -> Vec<PowJobStatus> {
82 self.jobs.values().map(|entry| PowJobStatus {
83 label: entry.label.clone(),
84 pow_min: entry.pow_min,
85 best_pow_so_far: entry.best_pow_so_far,
86 }).collect()
87 }
88}
89
90struct TrackedJobGuard {
93 tracker: Arc<Mutex<JobTracker>>,
94 job_id: JobId,
95}
96
97impl TrackedJobGuard {
98 fn new(tracker: Arc<Mutex<JobTracker>>, label: &str, pow_min: Pow) -> Self {
99 let now = RealTimeProvider.current_time_millis().0;
100 let job_id = {
101 let mut tracker = tracker.lock().unwrap();
102 let job_id = tracker.add(label, pow_min);
103 tracker.mark_work(now);
104 job_id
105 };
106 Self { tracker, job_id }
107 }
108
109 fn update(&self, best_pow_so_far: Pow) {
110 self.tracker.lock().unwrap().update(self.job_id, best_pow_so_far);
111 }
112}
113
114impl Drop for TrackedJobGuard {
115 fn drop(&mut self) {
116 let now = RealTimeProvider.current_time_millis().0;
119 let mut tracker = self.tracker.lock().unwrap();
120 tracker.remove(self.job_id);
121 tracker.mark_work(now);
122 }
123}
124
125#[async_trait::async_trait]
150pub trait PowGenerator: Send + Sync {
151 fn pool_size(&self) -> usize;
155
156 async fn run_chunk(&self, slot: usize, chunk_iterations: usize, pow_min: Pow, data_hash: Hash) -> anyhow::Result<(Salt, Pow, Hash)>;
162
163 fn tracker(&self) -> &Arc<Mutex<JobTracker>>;
167
168 fn cancellation_token(&self) -> Option<&CancellationToken> {
173 None
174 }
175
176 fn active_jobs(&self) -> Vec<PowJobStatus> {
178 self.tracker().lock().unwrap().snapshot()
179 }
180
181 fn is_pow_busy(&self, within_millis: i64) -> bool {
185 let now = RealTimeProvider.current_time_millis().0;
186 self.tracker().lock().unwrap().is_busy(now, within_millis)
187 }
188
189 async fn generate_best_effort(&self, label: &str, iteration_limit: usize, pow_min: Pow, data_hash: Hash) -> anyhow::Result<(Salt, Pow, Hash)> {
194 run_pool(self, label, Some(iteration_limit), pow_min, data_hash).await
195 }
196
197 async fn generate(&self, label: &str, pow_min: Pow, data_hash: Hash) -> anyhow::Result<(Salt, Pow, Hash)> {
200 run_pool(self, label, None, pow_min, data_hash).await
201 }
202}
203
204const CHUNK_ITERATIONS: usize = 4 * 1024;
210
211type SlotFuture<'a> = Pin<Box<dyn Future<Output = (usize, usize, anyhow::Result<(Salt, Pow, Hash)>)> + Send + 'a>>;
212
213pub async fn run_pool<'a, G: PowGenerator + ?Sized>(
226 generator: &'a G,
227 label: &'a str,
228 iteration_cap: Option<usize>,
229 pow_min: Pow,
230 data_hash: Hash,
231) -> anyhow::Result<(Salt, Pow, Hash)> {
232 let cancellation_token = generator.cancellation_token();
233 if cancellation_token.is_some_and(|cancellation_token| cancellation_token.is_cancelled()) {
234 anyhow::bail!("proof-of-work '{}' cancelled", label);
235 }
236
237 let tracker = generator.tracker().clone();
238 let guard = TrackedJobGuard::new(tracker, label, pow_min);
239
240 let real_time_provider = RealTimeProvider;
241 let mut estimator = PowRequiredEstimator::new(real_time_provider.current_time_millis(), label, pow_min);
242
243 let pool_size = generator.pool_size().max(1);
244 let mut remaining_iterations: Option<usize> = iteration_cap;
245
246 let mut best = {
253 let seed_salt = Salt::random();
254 let (seed_pow, seed_hash) = pow_measure_from_data_hash(&data_hash, &seed_salt)?;
255 (seed_salt, seed_pow, seed_hash)
256 };
257
258 guard.update(best.1);
259
260 if best.1 >= pow_min {
262 return Ok(best);
263 }
264
265 let mut in_flight: FuturesUnordered<SlotFuture<'a>> = FuturesUnordered::new();
266
267 for slot in 0..pool_size {
269 let chunk_size = pick_next_chunk_size(&mut remaining_iterations);
270 if chunk_size == 0 {
271 break;
272 }
273
274 in_flight.push(Box::pin(async move {
275 let chunk_result = generator.run_chunk(slot, chunk_size, pow_min, data_hash).await;
276 (slot, chunk_size, chunk_result)
277 }));
278 }
279
280 loop {
282 let next_chunk = match cancellation_token {
285 Some(cancellation_token) => {
286 let cancelled = std::pin::pin!(cancellation_token.cancelled());
287 match futures::future::select(in_flight.next(), cancelled).await {
288 Either::Left((next_chunk, _)) => next_chunk,
289 Either::Right(_) => anyhow::bail!("proof-of-work '{}' cancelled", label),
290 }
291 }
292 None => in_flight.next().await,
293 };
294 let Some((slot, chunk_size, chunk_result)) = next_chunk else {
295 break;
296 };
297
298 let chunk_best = chunk_result?;
299 if chunk_best.1 > best.1 {
300 best = chunk_best;
301 guard.update(best.1);
302 }
303 if best.1 >= pow_min {
304 return Ok(best);
305 }
306 if let Some(progress) = estimator.record_batch_and_estimate(real_time_provider.current_time_millis(), chunk_size, best.1) {
307 trace!("{}", progress);
308 }
309
310 let next_chunk_size = pick_next_chunk_size(&mut remaining_iterations);
311 if next_chunk_size == 0 {
312 continue;
313 }
314
315 in_flight.push(Box::pin(async move {
316 let chunk_result = generator.run_chunk(slot, next_chunk_size, pow_min, data_hash).await;
317 (slot, next_chunk_size, chunk_result)
318 }));
319 }
320
321 Ok(best)
322}
323
324fn pick_next_chunk_size(remaining_iterations: &mut Option<usize>) -> usize {
325 match remaining_iterations {
326 Some(0) => 0,
327 Some(remaining) => {
328 let chunk_size = (*remaining).min(CHUNK_ITERATIONS);
329 *remaining -= chunk_size;
330 chunk_size
331 }
332 None => CHUNK_ITERATIONS,
333 }
334}
335
336pub fn run_pool_chunk(chunk_iterations: usize, pow_min: Pow, data_hash: Hash) -> anyhow::Result<(Salt, Pow, Hash)> {
337 let mut best = {
338 let salt = Salt::random();
339 let (pow, hash) = pow_measure_from_data_hash(&data_hash, &salt)?;
340 (salt, pow, hash)
341 };
342
343 if best.1 >= pow_min {
344 return Ok(best);
345 }
346
347 for _ in 1..chunk_iterations {
348 let salt = Salt::random();
349 let (pow, hash) = pow_measure_from_data_hash(&data_hash, &salt)?;
350 if pow > best.1 {
351 best = (salt, pow, hash);
352 if best.1 >= pow_min {
353 return Ok(best);
354 }
355 }
356 }
357
358 Ok(best)
359}
360
361#[cfg(test)]
362mod tests {
363 use crate::tools::pow_generator::pow_generator::{JobTracker, TrackedJobGuard};
364 use crate::tools::types::Pow;
365 use std::sync::{Arc, Mutex};
366
367 #[test]
368 fn job_tracker_round_trip() {
369 let mut tracker = JobTracker::default();
370 assert!(tracker.snapshot().is_empty());
371
372 let job_a = tracker.add("rpc", Pow(18));
373 let job_b = tracker.add("post", Pow(22));
374
375 tracker.update(job_a, Pow(7));
376 tracker.update(job_b, Pow(13));
377 tracker.update(99999, Pow(255)); let mut snapshot = tracker.snapshot();
380 snapshot.sort_by(|a, b| a.label.cmp(&b.label));
381 assert_eq!(snapshot.len(), 2);
382 assert_eq!(snapshot[0].label, "post");
383 assert_eq!(snapshot[0].pow_min, Pow(22));
384 assert_eq!(snapshot[0].best_pow_so_far, Pow(13));
385 assert_eq!(snapshot[1].label, "rpc");
386 assert_eq!(snapshot[1].pow_min, Pow(18));
387 assert_eq!(snapshot[1].best_pow_so_far, Pow(7));
388
389 tracker.remove(job_a);
390 let remaining = tracker.snapshot();
391 assert_eq!(remaining.len(), 1);
392 assert_eq!(remaining[0].label, "post");
393
394 tracker.remove(job_b);
395 assert!(tracker.snapshot().is_empty());
396 }
397
398 #[test]
399 fn tracked_job_guard_removes_on_drop() {
400 let tracker = Arc::new(Mutex::new(JobTracker::default()));
401 {
402 let _guard = TrackedJobGuard::new(tracker.clone(), "rpc", Pow(18));
403 assert_eq!(tracker.lock().unwrap().snapshot().len(), 1);
404 }
405 assert!(tracker.lock().unwrap().snapshot().is_empty());
406 }
407
408 #[test]
409 fn tracked_job_guard_update_writes_through() {
410 let tracker = Arc::new(Mutex::new(JobTracker::default()));
411 let guard = TrackedJobGuard::new(tracker.clone(), "rpc", Pow(18));
412 guard.update(Pow(42));
413 let snapshot = tracker.lock().unwrap().snapshot();
414 assert_eq!(snapshot.len(), 1);
415 assert_eq!(snapshot[0].label, "rpc");
416 assert_eq!(snapshot[0].pow_min, Pow(18));
417 assert_eq!(snapshot[0].best_pow_so_far, Pow(42));
418 }
419
420 #[test]
421 fn is_busy_tracks_active_jobs_and_recent_work_window() {
422 let mut tracker = JobTracker::default();
423
424 assert!(!tracker.is_busy(1_000_000, 1000));
426
427 let job = tracker.add("rpc", Pow(18));
429 tracker.mark_work(1_000_000);
430 assert!(tracker.is_busy(1_000_000, 1000));
431 assert!(tracker.is_busy(9_999_999, 1000)); tracker.remove(job);
435 tracker.mark_work(2_000_000);
436 assert!(tracker.is_busy(2_000_500, 1000)); assert!(tracker.is_busy(2_001_000, 1000)); assert!(!tracker.is_busy(2_001_500, 1000)); }
440
441 #[test]
442 fn tracked_job_guard_marks_work_via_real_clock() {
443 use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
444
445 let tracker = Arc::new(Mutex::new(JobTracker::default()));
446
447 {
449 let _guard = TrackedJobGuard::new(tracker.clone(), "rpc", Pow(18));
450 assert!(tracker.lock().unwrap().is_busy(0, 0));
451 }
452
453 assert!(tracker.lock().unwrap().snapshot().is_empty());
456 let now = RealTimeProvider.current_time_millis().0;
457 assert!(tracker.lock().unwrap().is_busy(now, 1000));
458 assert!(!tracker.lock().unwrap().is_busy(now + 60_000, 1000));
459 }
460
461 #[tokio::test]
462 async fn run_pool_returns_consistent_sample_when_iteration_limit_is_zero() {
463 use crate::tools::pow::{pow_compute_data_hash, pow_measure_from_data_hash};
464 use crate::tools::pow_generator::pow_generator::PowGenerator;
465 use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
466 use crate::tools::types::Pow;
467
468 let data_hash = pow_compute_data_hash(&[b"zero-budget"]);
471 let generator = SingleThreadedPowGenerator::new();
472 let (salt, achieved_pow, _) = generator.generate_best_effort("zero", 0, Pow(255), data_hash).await.unwrap();
473 let (recomputed_pow, _) = pow_measure_from_data_hash(&data_hash, &salt).unwrap();
474 assert_eq!(recomputed_pow, achieved_pow);
475 }
476
477 #[tokio::test]
478 async fn run_pool_tracker_clears_after_completion() {
479 use crate::tools::pow::pow_compute_data_hash;
480 use crate::tools::pow_generator::pow_generator::PowGenerator;
481 use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
482 use crate::tools::types::Pow;
483
484 let data_hash = pow_compute_data_hash(&[b"tracker-cleanup"]);
485 let generator = SingleThreadedPowGenerator::new();
486 let _ = generator.generate("clean", Pow(0), data_hash).await.unwrap();
488 assert!(generator.active_jobs().is_empty());
489 }
490
491 #[tokio::test]
492 async fn run_pool_returns_as_soon_as_pow_min_is_met() {
493 use crate::tools::pow::pow_compute_data_hash;
494 use crate::tools::pow_generator::pow_generator::PowGenerator;
495 use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
496 use crate::tools::types::Pow;
497
498 const POW_MIN: Pow = Pow(8);
499 let data_hash = pow_compute_data_hash(&[b"early-exit"]);
500 let generator = SingleThreadedPowGenerator::new();
501 let (_, achieved_pow, _) = generator.generate("early", POW_MIN, data_hash).await.unwrap();
502 assert!(achieved_pow >= POW_MIN);
503 }
504
505 #[tokio::test]
511 async fn run_pool_pow_min_zero_returns_consistent_salt_and_pow() {
512 use crate::tools::pow::{pow_compute_data_hash, pow_measure_from_data_hash};
513 use crate::tools::pow_generator::pow_generator::PowGenerator;
514 use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
515 use crate::tools::types::Pow;
516
517 let generator = SingleThreadedPowGenerator::new();
518 for trial in 0u32..256 {
519 let data_hash = pow_compute_data_hash(&[&trial.to_le_bytes()]);
520 let (salt, achieved_pow, _) = generator.generate_best_effort("regression", 1, Pow(0), data_hash).await.unwrap();
521 let (recomputed_pow, _) = pow_measure_from_data_hash(&data_hash, &salt).unwrap();
522 assert_eq!(recomputed_pow, achieved_pow, "trial {}: salt and pow drifted apart", trial);
523 }
524 }
525
526 #[tokio::test]
532 async fn run_pool_returns_consistent_sample_when_budget_exhausted() {
533 use crate::tools::pow::{pow_compute_data_hash, pow_measure_from_data_hash};
534 use crate::tools::pow_generator::pow_generator::PowGenerator;
535 use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
536 use crate::tools::types::Pow;
537
538 let generator = SingleThreadedPowGenerator::new();
539 for trial in 0u32..256 {
540 let data_hash = pow_compute_data_hash(&[b"exhaust", &trial.to_le_bytes()]);
541 let (salt, achieved_pow, _) = generator.generate_best_effort("exhaust", 1, Pow(255), data_hash).await.unwrap();
543 let (recomputed_pow, _) = pow_measure_from_data_hash(&data_hash, &salt).unwrap();
544 assert_eq!(recomputed_pow, achieved_pow, "trial {}: returned salt does not produce returned pow", trial);
545 }
546 }
547}