1//! Client support for the `v1` onion service proof of work scheme
23use crate::err::ProofOfWorkError;
4use std::time::Instant;
5use tor_async_utils::oneshot;
6use tor_async_utils::oneshot::Canceled;
7use tor_cell::relaycell::hs::pow::v1::ProofOfWorkV1;
8use tor_checkable::{timed::TimerangeBound, Timebound};
9use tor_hscrypto::pk::HsBlindId;
10use tor_hscrypto::pow::v1::{Effort, Instance, SolverInput};
11use tor_netdoc::doc::hsdesc::pow::v1::PowParamsV1;
12use tracing::debug;
1314/// Double effort at retry until this threshold.
15///
16/// This could be made configurable, but currently it's hardcoded in c-tor and documented in the
17/// spec as a recommended value.
18const CLIENT_POW_EFFORT_DOUBLE_UNTIL: Effort = Effort::new(1000);
1920/// Effort multiplier to use above the doubling threshold.
21///
22/// This could be made configurable, but currently it's hardcoded in c-tor and documented in the
23/// spec as a recommended value.
24const CLIENT_POW_RETRY_MULTIPLIER: f32 = 1.5;
2526/// Minimum effort for retries.
27///
28/// This could be made configurable, but currently it's hardcoded in c-tor and documented in the
29/// spec as a recommended value.
30const CLIENT_MIN_RETRY_POW_EFFORT: Effort = Effort::new(8);
3132/// Client maximum effort.
33///
34/// This could be made configurable, but currently it's hardcoded in c-tor and documented in the
35/// spec as a recommended value.
36const CLIENT_MAX_POW_EFFORT: Effort = Effort::new(10000);
3738/// Client-side state for the 'v1' scheme in particular
39///
40#[derive(Debug)]
41pub(super) struct HsPowClientV1 {
42/// Time limited puzzle instance
43instance: TimerangeBound<Instance>,
44/// Next effort to use
45effort: Effort,
46}
4748impl HsPowClientV1 {
49/// Initialize client state for the `v1` scheme
50 ///
51pub(super) fn new(hs_blind_id: &HsBlindId, params: &PowParamsV1) -> Self {
52Self {
53// Create a puzzle Instance for this Seed. It doesn't matter whether
54 // the seed is valid at this moment. The time bound is preserved, and
55 // it's checked before we use the seed at each retry.
56instance: params
57 .seed()
58 .to_owned()
59 .dangerously_map(|seed| Instance::new(hs_blind_id.to_owned(), seed)),
60// Enforce maximum effort right away
61effort: params
62 .suggested_effort()
63 .clamp(Effort::zero(), CLIENT_MAX_POW_EFFORT),
64 }
65 }
6667/// Increase effort in response to a failed connection attempt.
68 ///
69 /// If no proof of work scheme is in use or the effort cannot be increased, this has no effect.
70 ///
71 /// Specified in <https://spec.torproject.org/hspow-spec/common-protocol.html#client-timeout>
72 ///
73pub(super) fn increase_effort(&mut self) {
74let effort = if self.effort < CLIENT_POW_EFFORT_DOUBLE_UNTIL {
75self.effort.saturating_mul_u32(2)
76 } else {
77self.effort.saturating_mul_f32(CLIENT_POW_RETRY_MULTIPLIER)
78 };
79self.effort = effort.clamp(CLIENT_MIN_RETRY_POW_EFFORT, CLIENT_MAX_POW_EFFORT);
80 }
8182/// Run the `v1` solver on a thread, if the effort is nonzero
83 ///
84 /// Returns None if the effort was zero.
85 /// Returns an Err() if the solver experienced a runtime error,
86 /// or if the seed is expired.
87pub(super) async fn solve(&self) -> Result<Option<ProofOfWorkV1>, ProofOfWorkError> {
88if self.effort == Effort::zero() {
89return Ok(None);
90 }
91let instance = self.instance.as_ref().check_valid_now()?.clone();
92let mut input = SolverInput::new(instance, self.effort);
93// TODO: config option
94input.runtime(Default::default());
9596let start_time = Instant::now();
97debug!("beginning solve, {:?}", self.effort);
9899let (result_sender, result_receiver) = oneshot::channel();
100 std::thread::spawn(move || {
101let mut solver = input.solve(&mut rand::rng());
102let result = loop {
103match solver.run_step() {
104Err(e) => break Err(e),
105Ok(Some(result)) => break Ok(result),
106Ok(None) => (),
107 }
108if result_sender.is_canceled() {
109return;
110 }
111 };
112let _ = result_sender.send(result);
113 });
114115let result = match result_receiver.await {
116Ok(Ok(solution)) => Ok(Some(ProofOfWorkV1::new(
117 solution.nonce().to_owned(),
118 solution.effort(),
119 solution.seed_head(),
120 solution.proof_to_bytes(),
121 ))),
122Ok(Err(e)) => Err(ProofOfWorkError::Runtime(e.into())),
123Err(Canceled) => Err(ProofOfWorkError::SolverDisconnected),
124 };
125126let elapsed_time = start_time.elapsed();
127debug!(
128"solve complete, {:?} {:?} duration={}ms (ratio: {} ms)",
129 result.as_ref().map(|_| ()),
130self.effort,
131 elapsed_time.as_millis(),
132 (elapsed_time.as_millis() as f32) / (*self.effort.as_ref() as f32),
133 );
134 result
135 }
136}