//! Solver implementation for v1 client puzzles use crate::pow::v1::challenge::Challenge; use crate::pow::v1::{ err::RuntimeErrorV1, types::Effort, types::Instance, types::NONCE_LEN, types::Nonce, types::Solution, }; use equix::{EquiXBuilder, HashError, RuntimeOption, SolverMemory}; use rand::{CryptoRng, Rng, RngCore}; /// All inputs necessary to run the [`Solver`] #[derive(Debug, Clone)] pub struct SolverInput { /// The puzzle instance we're solving instance: Instance, /// Effort chosen by the client for this solver run effort: Effort, /// Configuration settings for Equi-X, as an [`EquiXBuilder`] instance equix: EquiXBuilder, } impl SolverInput { /// Construct a [`SolverInput`] by wrapping an [`Instance`]. pub fn new(instance: Instance, effort: Effort) -> Self { SolverInput { instance, effort, equix: Default::default(), } } /// Select the HashX runtime to use for this Solver input. /// /// By default, uses [`RuntimeOption::TryCompile`]. pub fn runtime(&mut self, option: RuntimeOption) -> &mut Self { self.equix.runtime(option); self } /// Begin solving with this input and a new random [`Nonce`]. /// /// Generates a new random [`Nonce`] using the provided [`Rng`]. /// May be parallelized if desired, by cloning the [`SolverInput`] first. pub fn solve(self, rng: &mut R) -> Solver { self.solve_with_nonce(&rng.random::<[u8; NONCE_LEN]>().into()) } /// Begin solving with a specified [`Nonce`]. /// /// This is not generally useful, but it's great for unit tests if you'd /// like to skip to a deterministic location in the search. pub fn solve_with_nonce(self, nonce: &Nonce) -> Solver { Solver { challenge: Challenge::new(&self.instance, self.effort, nonce), equix: self.equix, mem: SolverMemory::new(), } } } /// Make progress toward finding a [`Solution`]. /// /// Each [`Solver`] instance will own about 1.8 MB of temporary memory until /// it is dropped. This interface supports cancelling an ongoing solve and it /// supports multithreaded use, but it requires an external thread pool /// implementation. pub struct Solver { /// The next assembled [`Challenge`] to try challenge: Challenge, /// Configuration settings for Equi-X, as an [`EquiXBuilder`] instance equix: EquiXBuilder, /// Temporary memory for Equi-X to use mem: SolverMemory, } impl Solver { /// Run the solver until it produces a [`Solution`]. /// /// This takes a random amount of time to finish, with no possibility /// to cancel early. If you need cancellation, use [`Self::run_step()`] /// instead. pub fn run(&mut self) -> Result { loop { if let Some(solution) = self.run_step()? { return Ok(solution); } } } /// Run the solver algorithm, returning when we are at a good stopping point. /// /// Typical durations would be very roughly 10ms with the compiled hash /// implementation or 250ms with the interpreted implementation. /// /// These durations are far too long to include in any event loop /// that's not built specifically for blocking operations, but they're /// short enough that we still have a chance of cancelling a high-effort /// solve. Step duration does not depend on effort choice. /// /// Internally, this checks only one [`Nonce`] value. That's the only good /// stopping point we have in Equi-X right now. If we really need finer /// grained cancellation the equix crate could be modified to support /// this but at a performance penalty. /// /// It's possible to call this again after a solution has already /// been returned, but the resulting solutions will have nearby [`Nonce`] /// values so this is not recommended except for benchmarking. pub fn run_step(&mut self) -> Result, RuntimeErrorV1> { match self.equix.build(self.challenge.as_ref()) { Ok(equix) => { for candidate in equix.solve_with_memory(&mut self.mem) { if self.challenge.check_effort(&candidate.to_bytes()).is_ok() { return Ok(Some(Solution::new( self.challenge.nonce(), self.challenge.effort(), self.challenge.seed().head(), candidate, ))); } } } Err(equix::Error::Hash(HashError::ProgramConstraints)) => (), Err(e) => { return Err(e.into()); } }; self.challenge.increment_nonce(); Ok(None) } }