Skip to main content

hashiverse_lib/tools/
pow_required_estimator.rs

1//! # Progress reporting and ETA estimation for long PoW searches
2//!
3//! A book-keeping helper for [`crate::tools::parallel_pow_generator`]: tracks how many
4//! attempts have been made across repeated batches, what the best-so-far `pow` is, and
5//! produces a human-readable log line including an ETA.
6//!
7//! Because PoW search is memoryless (geometric), past attempts give no information about
8//! how many attempts remain — the *expected remaining* is always `2^pow_required` no
9//! matter how long you've already been trying. The estimator uses rate of attempts per
10//! second to turn that into wall-clock seconds. For a geometric distribution the standard
11//! deviation equals the mean, so "30s ± 30s" ETAs are the norm; this is an honest report,
12//! not a bug.
13//!
14//! The `best_pow_so_far` field doubles as a sanity check: after `n` attempts the
15//! expected maximum leading-zero count is `log2(n) - 0.83`. A value wildly below that
16//! suggests a broken RNG or hash chain.
17
18use crate::tools::time::{DurationMillis, TimeMillis, MILLIS_IN_MILLISECOND};
19use crate::tools::types::Pow;
20
21/// Tracks progress across repeated calls to `pow_generate_with_iteration_limit`
22/// and produces a log-friendly ETA estimate.
23///
24/// PoW search is a memoryless geometric process: the *expected remaining* attempts
25/// is always `2^pow_required` regardless of how many have already been tried.
26/// Consequently:
27///   ETA_remaining = 2^pow_required / rate - elapsed
28///   std_deviation  = 2^pow_required / rate  (equals the mean for a geometric distribution)
29///
30/// `best_pow_so_far` is a sanity check: after `n` iterations the expected maximum
31/// leading-zero count is `log2(n) - 0.83`.  A value far below that suggests a
32/// broken RNG or hash chain.
33pub struct PowRequiredEstimator {
34    description: String,
35    pow_required: Pow,
36    total_iterations: usize,
37    best_pow_so_far: Pow,
38    started_at_millis: TimeMillis,
39}
40
41impl PowRequiredEstimator {
42    pub fn new(started_at_millis: TimeMillis, description: &str, pow_required: Pow) -> Self {
43        Self {
44            description: description.to_string(),
45            pow_required,
46            total_iterations: 0,
47            best_pow_so_far: Pow(0),
48            started_at_millis,
49        }
50    }
51
52    fn report_large_number(num: usize) -> String {
53        let num = num as f64;
54
55        if num > 1e9 {
56            return format!("{:.2}B", num / 1e9);
57        }
58        if num > 1e6 {
59            return format!("{:.2}M", num / 1e6);
60        }
61        if num > 1e3 {
62            return format!("{:.2}k", num / 1e3);
63        }
64
65        format!("{}", num)
66    }
67
68    /// Record the results of one batch and return a progress string suitable for logging.
69    pub fn record_batch_and_estimate(&mut self, current_time_millis: TimeMillis, iterations_in_batch: usize, best_pow_in_batch: Pow) -> String {
70        self.total_iterations += iterations_in_batch;
71        if best_pow_in_batch > self.best_pow_so_far {
72            self.best_pow_so_far = best_pow_in_batch;
73        }
74
75        let elapsed_duration_millis = current_time_millis - self.started_at_millis;
76
77        if elapsed_duration_millis < MILLIS_IN_MILLISECOND || self.total_iterations == 0 {
78            return format!(
79                "{}: PoW {}/{} bits | {} | {} iters | too early to estimate",
80                self.description,
81                self.best_pow_so_far.0,
82                self.pow_required.0,
83                elapsed_duration_millis,
84                Self::report_large_number(self.total_iterations)
85            );
86        }
87
88        let iterations_per_second = 1000.0 * self.total_iterations as f64 / elapsed_duration_millis.0 as f64;
89        // Remaining ETA: memoryless property means expected remaining = 2^pow_required
90        let expected_total_iterations = (2.0f64).powi(self.pow_required.0 as i32);
91        let expected_total_duration = DurationMillis((1000.0 * expected_total_iterations / iterations_per_second) as i64);
92        let eta_remaining_millis = expected_total_duration - elapsed_duration_millis;
93        // Std deviation of a geometric distribution equals its mean
94        let eta_one_sigma = expected_total_duration;
95        let progress_pct = self.total_iterations as f64 / expected_total_iterations * 100.0;
96
97        format!(
98            "{}: PoW {}/{} bits | {} | {} iters | {}/s | {:.1}% of expected | ETA ~{} \u{00b1}{}",
99            self.description,
100            self.best_pow_so_far.0,
101            self.pow_required.0,
102            elapsed_duration_millis,
103            Self::report_large_number(self.total_iterations),
104            Self::report_large_number(iterations_per_second as usize),
105            progress_pct,
106            eta_remaining_millis,
107            eta_one_sigma,
108        )
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    fn make_estimator() -> PowRequiredEstimator {
117        PowRequiredEstimator::new(TimeMillis(0), "test", Pow(24))
118    }
119
120    #[test]
121    fn record_batch_accumulates_iterations_and_tracks_best_pow() {
122        let mut estimator = make_estimator();
123
124        estimator.record_batch_and_estimate(TimeMillis(100), 1024, Pow(10));
125        assert_eq!(estimator.total_iterations, 1024);
126        assert_eq!(estimator.best_pow_so_far, Pow(10));
127
128        estimator.record_batch_and_estimate(TimeMillis(200), 1024, Pow(8)); // worse — should not update best
129        assert_eq!(estimator.total_iterations, 2048);
130        assert_eq!(estimator.best_pow_so_far, Pow(10));
131
132        estimator.record_batch_and_estimate(TimeMillis(300), 1024, Pow(15)); // better — should update best
133        assert_eq!(estimator.total_iterations, 3072);
134        assert_eq!(estimator.best_pow_so_far, Pow(15));
135    }
136
137    #[test]
138    fn progress_string_contains_key_fields() {
139        let mut estimator = make_estimator();
140        let output = estimator.record_batch_and_estimate(TimeMillis(1000), 65536, Pow(18));
141
142        assert!(output.contains("test"), "should include description: {}", output);
143        assert!(output.contains("18/24"), "should show best/required bits: {}", output);
144        assert!(output.contains("1s"), "should show elapsed time: {}", output);
145        assert!(output.contains("65.54"), "should show iteration count: {}", output);
146        assert!(output.contains("ETA"), "should show ETA: {}", output);
147    }
148
149    #[test]
150    fn progress_string_before_any_elapsed_time() {
151        let mut estimator = make_estimator();
152        let output = estimator.record_batch_and_estimate(TimeMillis(0), 1024, Pow(5));
153        assert!(output.contains("too early"), "{}", output);
154    }
155}