tor_hsclient/pow/
v1.rs

1//! Client support for the `v1` onion service proof of work scheme
2
3use 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;
13
14/// 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);
19
20/// 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;
25
26/// 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);
31
32/// 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);
37
38/// Client-side state for the 'v1' scheme in particular
39///
40#[derive(Debug)]
41pub(super) struct HsPowClientV1 {
42    /// Time limited puzzle instance
43    instance: TimerangeBound<Instance>,
44    /// Next effort to use
45    effort: Effort,
46}
47
48impl HsPowClientV1 {
49    /// Initialize client state for the `v1` scheme
50    ///
51    pub(super) fn new(hs_blind_id: &HsBlindId, params: &PowParamsV1) -> Self {
52        Self {
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.
56            instance: params
57                .seed()
58                .to_owned()
59                .dangerously_map(|seed| Instance::new(hs_blind_id.to_owned(), seed)),
60            // Enforce maximum effort right away
61            effort: params
62                .suggested_effort()
63                .clamp(Effort::zero(), CLIENT_MAX_POW_EFFORT),
64        }
65    }
66
67    /// 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    ///
73    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    /// 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.
87    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        // TODO: config option
94        input.runtime(Default::default());
95
96        let start_time = Instant::now();
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}