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}