1
//! Client support for the `v1` onion service proof of work scheme
2

            
3
use crate::err::ProofOfWorkError;
4
use std::time::Instant;
5
use tor_async_utils::oneshot;
6
use tor_async_utils::oneshot::Canceled;
7
use tor_cell::relaycell::hs::pow::v1::ProofOfWorkV1;
8
use tor_checkable::{timed::TimerangeBound, Timebound};
9
use tor_hscrypto::pk::HsBlindId;
10
use tor_hscrypto::pow::v1::{Effort, Instance, SolverInput};
11
use tor_netdoc::doc::hsdesc::pow::v1::PowParamsV1;
12
use 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.
18
const 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.
24
const 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.
30
const 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.
36
const CLIENT_MAX_POW_EFFORT: Effort = Effort::new(10000);
37

            
38
/// Client-side state for the 'v1' scheme in particular
39
///
40
#[derive(Debug)]
41
pub(super) struct HsPowClientV1 {
42
    /// Time limited puzzle instance
43
    instance: TimerangeBound<Instance>,
44
    /// Next effort to use
45
    effort: Effort,
46
}
47

            
48
impl 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
}