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 let instance = self.instance.as_ref().check_valid_now()?.clone();
92 let mut input = SolverInput::new(instance, self.effort);
93 input.runtime(Default::default());
95
96 let start_time = Instant::get();
97 debug!("beginning solve, {:?}", self.effort);
98
99 let (result_sender, result_receiver) = oneshot::channel();
100 std::thread::spawn(move || {
101 let mut solver = input.solve(&mut rand::rng());
102 let result = loop {
103 match solver.run_step() {
104 Err(e) => break Err(e),
105 Ok(Some(result)) => break Ok(result),
106 Ok(None) => (),
107 }
108 if result_sender.is_canceled() {
109 return;
110 }
111 };
112 let _ = result_sender.send(result);
113 });
114
115 let result = match result_receiver.await {
116 Ok(Ok(solution)) => Ok(Some(ProofOfWorkV1::new(
117 solution.nonce().to_owned(),
118 solution.effort(),
119 solution.seed_head(),
120 solution.proof_to_bytes(),
121 ))),
122 Ok(Err(e)) => Err(ProofOfWorkError::Runtime(e.into())),
123 Err(Canceled) => Err(ProofOfWorkError::SolverDisconnected),
124 };
125
126 let elapsed_time = start_time.elapsed();
127 debug!(
128 "solve complete, {:?} {:?} duration={}ms (ratio: {} ms)",
129 result.as_ref().map(|_| ()),
130 self.effort,
131 elapsed_time.as_millis(),
132 (elapsed_time.as_millis() as f32) / (*self.effort.as_ref() as f32),
133 );
134 result
135 }
136}