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