1
//! Solver implementation for v1 client puzzles
2

            
3
use crate::pow::v1::challenge::Challenge;
4
use crate::pow::v1::{
5
    err::RuntimeErrorV1, types::Effort, types::Instance, types::Nonce, types::Solution,
6
    types::NONCE_LEN,
7
};
8
use equix::{EquiXBuilder, HashError, RuntimeOption, SolverMemory};
9
use rand::{CryptoRng, Rng, RngCore};
10

            
11
/// All inputs necessary to run the [`Solver`]
12
#[derive(Debug, Clone)]
13
pub 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

            
22
impl SolverInput {
23
    /// Construct a [`SolverInput`] by wrapping an [`Instance`].
24
440
    pub fn new(instance: Instance, effort: Effort) -> Self {
25
440
        SolverInput {
26
440
            instance,
27
440
            effort,
28
440
            equix: Default::default(),
29
440
        }
30
440
    }
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
440
    pub fn solve_with_nonce(self, nonce: &Nonce) -> Solver {
53
440
        Solver {
54
440
            challenge: Challenge::new(&self.instance, self.effort, nonce),
55
440
            equix: self.equix,
56
440
            mem: SolverMemory::new(),
57
440
        }
58
440
    }
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.
67
pub 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

            
76
impl 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
440
    pub fn run(&mut self) -> Result<Solution, RuntimeErrorV1> {
83
        loop {
84
946
            if let Some(solution) = self.run_step()? {
85
440
                return Ok(solution);
86
506
            }
87
        }
88
440
    }
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
946
    pub fn run_step(&mut self) -> Result<Option<Solution>, RuntimeErrorV1> {
109
946
        match self.equix.build(self.challenge.as_ref()) {
110
946
            Ok(equix) => {
111
1826
                for candidate in equix.solve_with_memory(&mut self.mem) {
112
1826
                    if self.challenge.check_effort(&candidate.to_bytes()).is_ok() {
113
440
                        return Ok(Some(Solution::new(
114
440
                            self.challenge.nonce(),
115
440
                            self.challenge.effort(),
116
440
                            self.challenge.seed().head(),
117
440
                            candidate,
118
440
                        )));
119
1386
                    }
120
                }
121
            }
122
            Err(equix::Error::Hash(HashError::ProgramConstraints)) => (),
123
            Err(e) => {
124
                return Err(e.into());
125
            }
126
        };
127
506
        self.challenge.increment_nonce();
128
506
        Ok(None)
129
946
    }
130
}