Skip to main content

pow_account/
lib.rs

1//! # POW Account
2//!
3//! This library generates a cryptographic hash and performs a second round of hashing to produce a hash with a configurable number of leading zeros.
4//! It is designed for applications requiring proof-of-work-like functionality or hash-based validation with adjustable difficulty.
5//!
6//!
7//! ## Example Usage
8//! ```rust
9//! use pow_account::HashFinder;
10//!
11//! fn main() {
12//!     let origin_hash = HashFinder::new(4).find();
13//!     let origin_hash_hex = hex::encode(origin_hash);
14//!
15//!     if let Ok(target_hash_match_result) = HashFinder::new(4).check(origin_hash_hex) {
16//!         println!("Result of match: {}", target_hash_match_result);
17//!     }
18//! }
19//! ```
20//!
21//! ## Additional Information
22//! For more details, refer to the [README](https://github.com/1prefix/pow-account/blob/main/README.md).
23
24use blake2::{Blake2s256, Digest};
25use rand_core::{OsRng, RngCore};
26
27struct Entropy {
28    entropy: [u8; 32],
29}
30
31impl Entropy {
32    fn new() -> Self {
33        let mut entropy = [0u8; 32];
34        OsRng::default().fill_bytes(&mut entropy);
35
36        Entropy { entropy }
37    }
38
39    fn from(entropy: [u8; 32]) -> Self {
40        Entropy { entropy }
41    }
42
43    fn hash(&self) -> [u8; 32] {
44        let mut hash = Blake2s256::new();
45        let _ = hash.update(self.entropy);
46        hash.finalize().into()
47    }
48}
49
50struct HashPrefix {
51    zero_bits: u8,
52}
53
54impl Default for HashPrefix {
55    fn default() -> Self {
56        HashPrefix { zero_bits: 20u8 }
57    }
58}
59
60impl HashPrefix {
61    fn new(leading_zeros: u8) -> Self {
62        HashPrefix {
63            zero_bits: 8 / 2 * leading_zeros,
64        }
65    }
66}
67
68impl HashPrefix {
69    fn get(&self) -> u8 {
70        self.zero_bits
71    }
72
73    fn target(&self) -> [u8; 32] {
74        let target: [u8; 16] = self.target_u128().to_be_bytes();
75        let mut array: [u8; 32] = [255u8; 32];
76        array[0..16].copy_from_slice(target.as_slice());
77        array
78    }
79
80    fn target_u128(&self) -> u128 {
81        let total_bits = 128;
82        let remaining_bits = total_bits - self.get();
83
84        match self.get() < total_bits {
85            true => (1u128 << remaining_bits) - 1,
86            false => 1,
87        }
88    }
89}
90
91/// `HashFinder` is a Structure for finding cryptographic hashes that meet a specified difficulty target, defined by a number of leading zeros.
92/// The core idea is to search for a hash that is lower than a computed target value.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
94pub struct HashFinder {
95    target: [u8; 32],
96}
97
98impl Default for HashFinder {
99    fn default() -> Self {
100        HashFinder {
101            target: HashPrefix::default().target(),
102        }
103    }
104}
105
106impl HashFinder {
107    /// Returns a HashFinder struct with a specified number of target leading zeros
108    /// # Example
109    /// ```
110    /// use pow_account::HashFinder;
111    ///
112    /// let hash_finder = HashFinder::new(4);
113    ///
114    /// ```
115    pub fn new(leading_zeros: u8) -> Self {
116        HashFinder {
117            target: HashPrefix::new(leading_zeros).target(),
118        }
119    }
120
121    /// Finds an origin hash
122    ///
123    /// This function attempts to find a cryptographic hash that is an origin for a target hash that has a specific number of leading zeroes
124    ///
125    /// # Parameters
126    ///
127    /// - `leading_zeros`: The number of leading zeros to generate in the hash.
128    ///
129    /// # Returns
130    ///
131    /// This function returns a 32-byte array containing the generated hash.
132    ///
133    /// # Example
134    ///
135    /// ```rust
136    /// use pow_account::HashFinder;
137    /// use blake2::{Blake2s256, Digest};
138    ///
139    /// let origin_hash = HashFinder::new(4).find();
140    ///
141    /// let mut hasher = Blake2s256::new();
142    /// hasher.update(&origin_hash);
143    /// let target_hash: [u8; 32] = hasher.finalize().into();
144    ///
145    /// let target_hash_hex = hex::encode(target_hash);
146    ///
147    /// assert!(target_hash_hex.starts_with("0000"));
148    /// ```
149    pub fn find(&self) -> [u8; 32] {
150        loop {
151            let origin_hash = Entropy::new().hash();
152            let target_hash = Entropy::from(origin_hash).hash();
153            match target_hash < self.target {
154                true => return origin_hash,
155                false => continue,
156            }
157        }
158    }
159
160    /// Determines whether a given hash serves as the origin for a target hash that satisfies a specified number of leading zeros.
161    ///
162    /// This function takes a hexadecimal string representing an origin hash and checks if
163    /// its hash satisfies the leading zero requirement specified by the `leading_zeros` value.
164    ///
165    /// # Parameters
166    ///
167    /// - `hash`: A string containing the hexadecimal representation of the hash to check.
168    ///
169    /// # Returns
170    ///
171    /// This function returns `Ok(true)` if the hash meets the requirement, otherwise
172    /// it returns `Ok(false)`. If the input string is not a valid hexadecimal representation,
173    /// it returns an `Err` with a description of the error.
174    ///
175    /// # Errors
176    ///
177    /// This function returns an error if the provided hash string is not a valid
178    /// hexadecimal representation.
179    ///
180    /// # Examples
181    /// ```
182    /// use pow_account::HashFinder;
183    ///
184    /// let origin_hash = HashFinder::new(4).find();
185    /// let origin_hash_hex = hex::encode(origin_hash);
186    /// let result = HashFinder::new(3).check(origin_hash_hex);
187    /// assert!(result.unwrap());
188    ///
189    /// let origin_hash = String::from("3ca727c7fefed674268797882ff4b26c8e28873ee6fbfae71d9ccc35e24444d4");
190    /// let result = HashFinder::new(3).check(origin_hash);
191    /// assert!(result.unwrap());
192    ///
193    /// let origin_hash = String::from("51ad0600f06b0d57300a37952cea658410488748400628c8a2e7d712892d806e");
194    /// let result = HashFinder::default().check(origin_hash);
195    /// assert!(result.unwrap());
196    /// ```
197    pub fn check(&self, origin_hash: String) -> Result<bool, hex::FromHexError> {
198        let mut origin_hash_bytes: [u8; 32] = [0u8; 32];
199        let _ = hex::decode_to_slice(origin_hash, &mut origin_hash_bytes)?;
200
201        let target_hash_bytes = Entropy::from(origin_hash_bytes).hash();
202
203        Ok(target_hash_bytes < self.target)
204    }
205}
206
207#[cfg(test)]
208mod pow_account {
209
210    use super::*;
211    use hex::FromHexError;
212
213    #[test]
214    fn new_entropy_has_a_length_of_32() {
215        let entropy = Entropy::new().entropy;
216        assert!(entropy.len().eq(&32))
217    }
218
219    #[test]
220    fn new_entropy_is_unique() {
221        let entropy_a = Entropy::new().entropy;
222        let entropy_b = Entropy::new().entropy;
223        assert_ne!(entropy_a, entropy_b)
224    }
225
226    #[test]
227    fn entropy_generates_256bit_hash() {
228        let hash = Entropy::new().hash();
229        assert!(hash.len().eq(&32))
230    }
231
232    #[test]
233    fn entropy_created_from_a_set_of_bytes() {
234        let entropy_a = Entropy::new().entropy;
235        let entropy_b = Entropy::from(entropy_a);
236        assert_eq!(entropy_b.entropy, entropy_a)
237    }
238
239    #[test]
240    fn blake2s_hash_can_be_validated() {
241        let origin_hash = "c37289b48949a7d172346cb3e5600da905f53e7c022d364836dcf57db4de33fa";
242        let target_hash = "7478987293e1864fd833ae3607bc99b9b22e7ca39bced21c3b0428bd9c7218ba";
243
244        let origin_hash_vec = hex::decode(origin_hash).unwrap();
245        let origin_hash_bytes: [u8; 32] = origin_hash_vec.try_into().unwrap();
246
247        let entropy = Entropy::from(origin_hash_bytes);
248        let origin_hash_hex = hex::encode(entropy.hash()).to_string();
249        assert_eq!(origin_hash_hex, target_hash)
250    }
251
252    #[test]
253    fn new_hash_prefix_has_non_zero_number_of_bits() {
254        let hash_prefix = HashPrefix::default();
255        assert!(hash_prefix.get() > 0)
256    }
257
258    #[test]
259    fn default_target_matches_max_value() {
260        let target = HashPrefix::default().target();
261        let max_target: [u8; 32] = [
262            0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
263            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
264            0xff, 0xff, 0xff, 0xff,
265        ];
266
267        assert_eq!(target, max_target)
268    }
269
270    #[test]
271    fn can_find_a_hash_which_starts_from_a_specific_pattern() {
272        let origin_hash = HashFinder::new(4).find();
273        let target_hash = Entropy::from(origin_hash).hash();
274
275        let hash_hex = hex::encode(target_hash);
276        assert!(hash_hex.starts_with("0000"))
277    }
278
279    #[test]
280    fn checks_the_hash_for_the_required_number_of_leading_zeros() {
281        let hash = String::from("3ca727c7fefed674268797882ff4b26c8e28873ee6fbfae71d9ccc35e24444d4");
282        assert!(HashFinder::new(3).check(hash).unwrap());
283
284        let hash = String::from("73b8f38be026335eb78946ea30434ff3cee4cff6544d49b4772f80397d40e72f");
285        assert!(HashFinder::new(4).check(hash).unwrap());
286
287        let hash = String::from("51ad0600f06b0d57300a37952cea658410488748400628c8a2e7d712892d806e");
288        assert!(HashFinder::new(5).check(hash).unwrap());
289
290        let hash = String::from("51ad0600f06b0d57300a37952cea658410488748400628c8a2e7d712892d806e");
291        assert!(HashFinder::default().check(hash).unwrap());
292
293        let hash = String::from("3ca727c7fefed674268797882ff4b26c8e28873ee6fbfae71d9ccc35e24444d4");
294        assert_eq!(HashFinder::new(4).check(hash).unwrap(), false);
295
296        let hash = String::from("3c+727c7fefed674268797882ff4b26c8e28873ee6fbfae71d9ccc35e24444d4");
297        let err = HashFinder::new(4).check(hash).unwrap_err();
298        assert_eq!(err, FromHexError::InvalidHexCharacter { c: '+', index: 2 });
299
300        let hash = String::from("3ca727c7fefed674268797882ff4b26c8e28873ee6fbfae71d9ccc35e24444d");
301        let err = HashFinder::new(4).check(hash).unwrap_err();
302        assert_eq!(err, FromHexError::OddLength);
303
304        let hash =
305            String::from("3ca727c7fefed674268797882ff4b26c8e28873ee6fbfae71d9ccc35e24444d444d4");
306        let err = HashFinder::new(4).check(hash).unwrap_err();
307        assert_eq!(err, FromHexError::InvalidStringLength)
308    }
309}