tor_hscrypto/pow/v1/
solve.rs

1//! Solver implementation for v1 client puzzles
2
3use crate::pow::v1::challenge::Challenge;
4use crate::pow::v1::{
5    err::RuntimeErrorV1, types::Effort, types::Instance, types::Nonce, types::Solution,
6    types::NONCE_LEN,
7};
8use equix::{EquiXBuilder, HashError, RuntimeOption, SolverMemory};
9use rand::{CryptoRng, Rng, RngCore};
10
11/// All inputs necessary to run the [`Solver`]
12#[derive(Debug, Clone)]
13pub struct SolverInput {
14    /// The puzzle instance we're solving
15    instance: Instance,
16    /// Effort chosen by the client for this solver run
17    effort: Effort,
18    /// Configuration settings for Equi-X, as an [`EquiXBuilder`] instance
19    equix: EquiXBuilder,
20}
21
22impl SolverInput {
23    /// Construct a [`SolverInput`] by wrapping an [`Instance`].
24    pub fn new(instance: Instance, effort: Effort) -> Self {
25        SolverInput {
26            instance,
27            effort,
28            equix: Default::default(),
29        }
30    }
31
32    /// Select the HashX runtime to use for this Solver input.
33    ///
34    /// By default, uses [`RuntimeOption::TryCompile`].
35    pub fn runtime(&mut self, option: RuntimeOption) -> &mut Self {
36        self.equix.runtime(option);
37        self
38    }
39
40    /// Begin solving with this input and a new random [`Nonce`].
41    ///
42    /// Generates a new random [`Nonce`] using the provided [`Rng`].
43    /// May be parallelized if desired, by cloning the [`SolverInput`] first.
44    pub fn solve<R: RngCore + CryptoRng>(self, rng: &mut R) -> Solver {
45        self.solve_with_nonce(&rng.random::<[u8; NONCE_LEN]>().into())
46    }
47
48    /// Begin solving with a specified [`Nonce`].
49    ///
50    /// This is not generally useful, but it's great for unit tests if you'd
51    /// like to skip to a deterministic location in the search.
52    pub fn solve_with_nonce(self, nonce: &Nonce) -> Solver {
53        Solver {
54            challenge: Challenge::new(&self.instance, self.effort, nonce),
55            equix: self.equix,
56            mem: SolverMemory::new(),
57        }
58    }
59}
60
61/// Make progress toward finding a [`Solution`].
62///
63/// Each [`Solver`] instance will own about 1.8 MB of temporary memory until
64/// it is dropped. This interface supports cancelling an ongoing solve and it
65/// supports multithreaded use, but it requires an external thread pool
66/// implementation.
67pub struct Solver {
68    /// The next assembled [`Challenge`] to try
69    challenge: Challenge,
70    /// Configuration settings for Equi-X, as an [`EquiXBuilder`] instance
71    equix: EquiXBuilder,
72    /// Temporary memory for Equi-X to use
73    mem: SolverMemory,
74}
75
76impl Solver {
77    /// Run the solver until it produces a [`Solution`].
78    ///
79    /// This takes a random amount of time to finish, with no possibility
80    /// to cancel early. If you need cancellation, use [`Self::run_step()`]
81    /// instead.
82    pub fn run(&mut self) -> Result<Solution, RuntimeErrorV1> {
83        loop {
84            if let Some(solution) = self.run_step()? {
85                return Ok(solution);
86            }
87        }
88    }
89
90    /// Run the solver algorithm, returning when we are at a good stopping point.
91    ///
92    /// Typical durations would be very roughly 10ms with the compiled hash
93    /// implementation or 250ms with the interpreted implementation.
94    ///
95    /// These durations are far too long to include in any event loop
96    /// that's not built specifically for blocking operations, but they're
97    /// short enough that we still have a chance of cancelling a high-effort
98    /// solve. Step duration does not depend on effort choice.
99    ///
100    /// Internally, this checks only one [`Nonce`] value. That's the only good
101    /// stopping point we have in Equi-X right now. If we really need finer
102    /// grained cancellation the equix crate could be modified to support
103    /// this but at a performance penalty.
104    ///
105    /// It's possible to call this again after a solution has already
106    /// been returned, but the resulting solutions will have nearby [`Nonce`]
107    /// values so this is not recommended except for benchmarking.
108    pub fn run_step(&mut self) -> Result<Option<Solution>, RuntimeErrorV1> {
109        match self.equix.build(self.challenge.as_ref()) {
110            Ok(equix) => {
111                for candidate in equix.solve_with_memory(&mut self.mem) {
112                    if self.challenge.check_effort(&candidate.to_bytes()).is_ok() {
113                        return Ok(Some(Solution::new(
114                            self.challenge.nonce(),
115                            self.challenge.effort(),
116                            self.challenge.seed().head(),
117                            candidate,
118                        )));
119                    }
120                }
121            }
122            Err(equix::Error::Hash(HashError::ProgramConstraints)) => (),
123            Err(e) => {
124                return Err(e.into());
125            }
126        };
127        self.challenge.increment_nonce();
128        Ok(None)
129    }
130}