1use crate::err::ProofOfWorkError;
4use tor_async_utils::oneshot;
5use tor_async_utils::oneshot::Canceled;
6use tor_cell::relaycell::hs::pow::v1::ProofOfWorkV1;
7use tor_checkable::{TimeBound, timed::TimeRangeBound};
8use tor_hscrypto::pk::HsBlindId;
9use tor_hscrypto::pow::v1::{Effort, Instance, SolverInput};
10use tor_netdoc::doc::hsdesc::pow::v1::PowParamsV1;
11use tracing::debug;
12use web_time_compat::{Instant, InstantExt};
13
14const CLIENT_POW_EFFORT_DOUBLE_UNTIL: Effort = Effort::new(1000);
19
20const CLIENT_POW_RETRY_MULTIPLIER: f32 = 1.5;
25
26const CLIENT_MIN_RETRY_POW_EFFORT: Effort = Effort::new(8);
31
32const CLIENT_MAX_POW_EFFORT: Effort = Effort::new(10000);
37
38#[derive(Debug)]
41pub(super) struct HsPowClientV1 {
42 instance: TimeRangeBound<Instance>,
44 effort: Effort,
46}
47
48impl HsPowClientV1 {
49 pub(super) fn new(hs_blind_id: &HsBlindId, params: &PowParamsV1) -> Self {
52 Self {
53 instance: params
57 .seed()
58 .to_owned()
59 .dangerously_map(|seed| Instance::new(hs_blind_id.to_owned(), seed)),
60 effort: params
62 .suggested_effort()
63 .clamp(Effort::zero(), CLIENT_MAX_POW_EFFORT),
64 }
65 }
66
67 pub(super) fn increase_effort(&mut self) {
74 let effort = if self.effort < CLIENT_POW_EFFORT_DOUBLE_UNTIL {
75 self.effort.saturating_mul_u32(2)
76 } else {
77 self.effort.saturating_mul_f32(CLIENT_POW_RETRY_MULTIPLIER)
78 };
79 self.effort = effort.clamp(CLIENT_MIN_RETRY_POW_EFFORT, CLIENT_MAX_POW_EFFORT);
80 }
81
82 pub(super) async fn solve(&self) -> Result<Option<ProofOfWorkV1>, ProofOfWorkError> {
88 if self.effort == Effort::zero() {
89 return Ok(None);
90 }
91 #[allow(clippy::disallowed_methods)] let instance = self.instance.as_ref().if_valid_now()?.clone();
93 let mut input = SolverInput::new(instance, self.effort);
94 input.runtime(Default::default());
96
97 let start_time = Instant::get();
98 debug!("beginning solve, {:?}", self.effort);
99
100 let (result_sender, result_receiver) = oneshot::channel();
101 std::thread::spawn(move || {
102 let mut solver = input.solve(&mut rand::rng());
103 let result = loop {
104 match solver.run_step() {
105 Err(e) => break Err(e),
106 Ok(Some(result)) => break Ok(result),
107 Ok(None) => (),
108 }
109 if result_sender.is_canceled() {
110 return;
111 }
112 };
113 let _ = result_sender.send(result);
114 });
115
116 let result = match result_receiver.await {
117 Ok(Ok(solution)) => Ok(Some(ProofOfWorkV1::new(
118 solution.nonce().to_owned(),
119 solution.effort(),
120 solution.seed_head(),
121 solution.proof_to_bytes(),
122 ))),
123 Ok(Err(e)) => Err(ProofOfWorkError::Runtime(e.into())),
124 Err(Canceled) => Err(ProofOfWorkError::SolverDisconnected),
125 };
126
127 let elapsed_time = start_time.elapsed();
128 debug!(
129 "solve complete, {:?} {:?} duration={}ms (ratio: {} ms)",
130 result.as_ref().map(|_| ()),
131 self.effort,
132 elapsed_time.as_millis(),
133 (elapsed_time.as_millis() as f32) / (*self.effort.as_ref() as f32),
134 );
135 result
136 }
137}