tor_hscrypto/pow/v1/types.rs
1//! Basic types used by the v1 client puzzle
2
3use crate::pk::HsBlindId;
4use crate::pow::v1::err::SolutionErrorV1;
5use rand::prelude::*;
6use serde::{Deserialize, Serialize};
7use tor_bytes::{EncodeResult, Readable, Reader, Writeable, Writer};
8
9/// Effort setting, a u32 value with linear scale
10///
11/// The numerical value is roughly the expected number of times we will
12/// need to invoke the underlying solver (Equi-X) for the v1 proof-of-work
13/// protocol to find a solution.
14#[derive(derive_more::AsRef, derive_more::From, derive_more::Into)] //
15#[derive(Serialize, Deserialize)] //
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
17pub struct Effort(u32);
18
19impl Effort {
20 /// Const constructor for `Effort` from any u32
21 ///
22 /// `Effort` can also be constructed using into() conversions,
23 /// but those are not available in const expressions.
24 ///
25 pub const fn new(value: u32) -> Self {
26 Self(value)
27 }
28
29 /// Const constructor for zero effort
30 ///
31 /// Zero effort is used in suggested effort to mean the puzzle is
32 /// available but shouldn't be used on a first connection attempt.
33 ///
34 /// If zero is actually used for solve and verify, it will have equivalent
35 /// performance to an effort of 1.
36 ///
37 pub const fn zero() -> Self {
38 Self(0)
39 }
40
41 /// Multiply this effort by an integer of the same size, saturating on overflow
42 pub const fn saturating_mul_u32(&self, rhs: u32) -> Self {
43 Self(self.0.saturating_mul(rhs))
44 }
45
46 /// Multiply this effort by a float, with saturating cast back to Effort
47 pub fn saturating_mul_f32(&self, rhs: f32) -> Self {
48 // 'as' cast from float to int saturates, and NaN becomes 0.
49 Self((self.0 as f32 * rhs) as u32)
50 }
51}
52
53/// Length of the random seed generated by servers and included in HsDir
54pub const SEED_LEN: usize = 32;
55
56/// The random portion of a challenge, distributed through HsDir
57#[derive(
58 derive_more::AsRef, derive_more::From, Deserialize, Serialize, Debug, Clone, Eq, PartialEq,
59)]
60pub struct Seed([u8; SEED_LEN]);
61
62impl Seed {
63 /// Generate a new, random seed.
64 ///
65 /// If old_seed is given, avoid generating a seed that shares a seed head.
66 pub fn new<R: Rng + CryptoRng>(rng: &mut R, old_seed: Option<&Seed>) -> Self {
67 let mut new = Seed(rng.random());
68 while Some(new.head()) == old_seed.as_ref().map(|seed| seed.head()) {
69 new = Seed(rng.random());
70 }
71 new
72 }
73
74 /// Make a new [`SeedHead`] from a prefix of this seed
75 pub fn head(&self) -> SeedHead {
76 SeedHead(
77 self.0[..SEED_HEAD_LEN]
78 .try_into()
79 .expect("slice length correct"),
80 )
81 }
82}
83
84impl std::fmt::Display for Seed {
85 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
86 write!(f, "{}", hex::encode(self.0))
87 }
88}
89
90/// Error when converting a string to a [`Seed`].
91#[non_exhaustive]
92pub enum ParseSeedError {
93 /// The string was not a valid hex string.
94 InvalidHex(hex::FromHexError),
95 /// The string was not the correct length for a [`Seed`].
96 InvalidLength,
97}
98
99impl std::str::FromStr for Seed {
100 type Err = ParseSeedError;
101
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
103 let bytes = hex::decode(s).map_err(ParseSeedError::InvalidHex)?;
104 Ok(Seed(
105 bytes
106 .try_into()
107 .map_err(|_| ParseSeedError::InvalidLength)?,
108 ))
109 }
110}
111
112/// Length of a seed prefix used to identify the entire seed
113pub const SEED_HEAD_LEN: usize = 4;
114
115/// A short seed prefix used in solutions to reference the complete seed
116#[derive(derive_more::AsRef, derive_more::From, Debug, Clone, Copy, Eq, PartialEq, Hash)]
117pub struct SeedHead([u8; SEED_HEAD_LEN]);
118
119/// Length of the nonce value generated by clients and included in the solution
120pub const NONCE_LEN: usize = 16;
121
122/// Generated randomly by solvers and included in the solution
123#[derive(derive_more::AsRef, derive_more::From, Debug, Clone, Eq, PartialEq)]
124pub struct Nonce([u8; NONCE_LEN]);
125
126/// Generate [`Readable`] and [`Writeable`] implementations for trivial newtype wrappers
127macro_rules! impl_readable_writeable_newtype {
128 ($t:ty) => {
129 impl Readable for $t {
130 fn take_from(b: &mut Reader<'_>) -> tor_bytes::Result<Self> {
131 Ok(Self(Readable::take_from(b)?))
132 }
133 }
134 impl Writeable for $t {
135 fn write_onto<B: Writer + ?Sized>(&self, b: &mut B) -> EncodeResult<()> {
136 self.0.write_onto(b)
137 }
138 }
139 };
140}
141
142impl_readable_writeable_newtype!(Effort);
143impl_readable_writeable_newtype!(Seed);
144impl_readable_writeable_newtype!(SeedHead);
145impl_readable_writeable_newtype!(Nonce);
146
147/// One instance of this proof-of-work puzzle
148///
149/// Identified uniquely by the combination of onion service blinded Id key
150/// plus a rotating seed chosen by the service.
151#[derive(Debug, Clone)]
152pub struct Instance {
153 /// Blinded public Id key, binding this puzzle to a specific onion service
154 service: HsBlindId,
155 /// Seed value distributed in the HsDir by that service
156 seed: Seed,
157}
158
159impl Instance {
160 /// A new puzzle instance, wrapping a service Id and service-chosen seed
161 pub fn new(service: HsBlindId, seed: Seed) -> Self {
162 Self { service, seed }
163 }
164
165 /// Get the [`HsBlindId`] identifying the service this puzzle is for.
166 pub fn service(&self) -> &HsBlindId {
167 &self.service
168 }
169
170 /// Get the rotating random [`Seed`] used in this puzzle instance.
171 pub fn seed(&self) -> &Seed {
172 &self.seed
173 }
174}
175
176/// One potential solution to some puzzle [`Instance`]
177///
178/// The existence of a [`Solution`] guarantees that the solution is well formed
179/// (for example, the correct length, the correct order in the Equi-X solution)
180/// but it makes no guarantee to actually solve any specific puzzle instance.
181#[derive(Debug, Clone, Eq, PartialEq)]
182pub struct Solution {
183 /// Arbitrary value chosen by the solver to reach a valid solution
184 ///
185 /// Services are responsible for remembering used values to prevent replay.
186 nonce: Nonce,
187
188 /// The effort chosen by the client
189 ///
190 /// This is validated against the actual effort spent by the client using
191 /// a combination of two checks:
192 ///
193 /// - We can ensure the effort value here was chosen prior to successfully
194 /// solving the Equi-X puzzle just by verifying the Equi-X proof.
195 /// Effort values are part of the [`crate::pow::v1::challenge::Challenge`]
196 /// string the puzzle is constructed around.
197 ///
198 /// - We can ensure, on average, that the proper proportion of Equi-X
199 /// solutions have been discarded. The proof and challenge are hashed,
200 /// and the resulting digest is effectively a random variable that must
201 /// fit within a range inversely proportional to the effort. This test
202 /// happens in [`crate::pow::v1::challenge::Challenge::check_effort`].
203 effort: Effort,
204
205 /// Prefix of the [`Seed`] used in this puzzle Instance
206 ///
207 /// A service will normally have two active [`Seed`] values at once.
208 /// This prefix is sufficient to distinguish between them. (Services
209 /// skip seeds which would have the same prefix as the last seed.)
210 seed_head: SeedHead,
211
212 /// Equi-X solution which claims to prove the above effort choice
213 proof: equix::Solution,
214}
215
216impl Solution {
217 /// Construct a new Solution around a well-formed [`equix::Solution`] proof.
218 pub fn new(nonce: Nonce, effort: Effort, seed_head: SeedHead, proof: equix::Solution) -> Self {
219 Solution {
220 nonce,
221 effort,
222 seed_head,
223 proof,
224 }
225 }
226
227 /// Try to build a [`Solution`] from an unvalidated [`equix::SolutionByteArray`].
228 ///
229 /// This will either return a [`Solution`] or a [`SolutionErrorV1::Order`].
230 pub fn try_from_bytes(
231 nonce: Nonce,
232 effort: Effort,
233 seed_head: SeedHead,
234 bytes: &equix::SolutionByteArray,
235 ) -> Result<Self, SolutionErrorV1> {
236 Ok(Self::new(
237 nonce,
238 effort,
239 seed_head,
240 equix::Solution::try_from_bytes(bytes).map_err(|_| SolutionErrorV1::Order)?,
241 ))
242 }
243
244 /// Get the winning [`Nonce`] value used in this solution
245 pub fn nonce(&self) -> &Nonce {
246 &self.nonce
247 }
248
249 /// Get the client-chosen and provable [`Effort`] value used in this solution
250 pub fn effort(&self) -> Effort {
251 self.effort
252 }
253
254 /// Get the [`SeedHead`] value identifying the puzzle this solution is for
255 pub fn seed_head(&self) -> SeedHead {
256 self.seed_head
257 }
258
259 /// Reference the [`equix::Solution`] backing the proof portion
260 pub fn proof(&self) -> &equix::Solution {
261 &self.proof
262 }
263
264 /// Clone the proof portion of the solution in its canonical byte string format
265 pub fn proof_to_bytes(&self) -> equix::SolutionByteArray {
266 self.proof.to_bytes()
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn new_seed_head_collision() {
276 use tor_basic_utils::test_rng::Config;
277
278 let mut rng = Config::Seeded([
279 191, 169, 93, 126, 223, 128, 184, 217, 105, 3, 80, 87, 181, 242, 206, 38, 152, 149,
280 116, 71, 249, 142, 6, 196, 188, 141, 20, 139, 97, 45, 230, 55,
281 ])
282 .into_rng();
283
284 let seed_1 = Seed::new(&mut rng, None);
285 let seed_2_shared_head = Seed::new(&mut rng, None);
286
287 // Verify that this RNG seed does actually result in a collision.
288 // If the code to generate the seed is changed, we may need to find a new RNG seed to
289 // generate this collision.
290 assert_eq!(seed_1.head(), seed_2_shared_head.head());
291
292 let mut rng = Config::Seeded([0x00; 32]).into_rng();
293
294 let seed_1 = Seed::new(&mut rng, None);
295 let seed_2 = Seed::new(&mut rng, Some(&seed_1));
296
297 assert_ne!(seed_1.head(), seed_2.head());
298 }
299}