Skip to main content

hashiverse_lib/tools/
pow.rs

1//! This crate provides a PoW mechanism that should be hugely expensive to reproduce on dedicated hardware.
2//! Given that Hashiverse is predominantly built upon proof of work, we want to put in some effort to make it difficult to cheat as a spammer or sybil using GPU or ASIC advantages.
3//! To this effect we cobble together a chain of different hashing algorithms with different repetition counts - all of which are pseudorandomly chosen based on the initial salt and each subsequent hashing round.
4
5use crate::tools::types::{Hash, Pow, Salt};
6use crate::tools::{hashing, tools};
7use digest::consts::{U32, U64};
8
9use digest::Digest;
10
11fn apply_hash<H>(data: &Hash) -> anyhow::Result<Hash>
12where H: Digest
13{
14    Hash::from_slice(&H::digest(data.as_ref()).as_slice()[0..32])
15}
16
17fn apply_chained_hash(algo_index: usize, hash_current: Hash) -> anyhow::Result<Hash> {
18
19    const ALGO_COUNT: usize = 17;
20    let algo_index = algo_index % ALGO_COUNT;
21
22    match algo_index {
23        0 => apply_hash::<blake2::Blake2s256>(&hash_current),
24        1 => apply_hash::<blake2::Blake2b512>(&hash_current),
25        2 => apply_hash::<sha2::Sha256>(&hash_current),
26        3 => apply_hash::<sha2::Sha384>(&hash_current),
27        4 => apply_hash::<sha2::Sha512>(&hash_current),
28        5 => apply_hash::<sha3::Sha3_256>(&hash_current),
29        6 => apply_hash::<sha3::Sha3_384>(&hash_current),
30        7 => apply_hash::<sha3::Sha3_512>(&hash_current),
31        8 => apply_hash::<sha3::Keccak256>(&hash_current),
32        9 => apply_hash::<sha3::Keccak384>(&hash_current),
33        10 => apply_hash::<sha3::Keccak512>(&hash_current),
34        11 => apply_hash::<groestl::Groestl256>(&hash_current),
35        12 => apply_hash::<groestl::Groestl512>(&hash_current),
36        13 => apply_hash::<whirlpool::Whirlpool>(&hash_current),
37        14 => apply_hash::<skein::Skein256<U32>>(&hash_current),
38        15 => apply_hash::<skein::Skein512<U64>>(&hash_current),
39
40        // Unfortunately the "digest" trait of the blake3 crate is an older version than we need...so hand roll.
41        16 => {
42            let mut hasher = blake3::Hasher::new();
43            hasher.update(hash_current.as_ref());
44            let hash_output = hasher.finalize();
45            Hash::from_slice(&hash_output.as_bytes()[0..32])
46        },
47
48        _ => Ok(hash_current),
49    }
50}
51
52/// Pre-hash all input data into a single 32-byte `Hash`.
53///
54/// Call this once before the iteration loop; pass the result to
55/// `pow_measure_from_data_hash` (and to `PowGenerator::generate*`) so that
56/// workers only receive 32 bytes instead of the full raw data.
57pub fn pow_compute_data_hash(datas: &[&[u8]]) -> Hash {
58    hashing::hash_multiple(datas)
59}
60
61/// Core PoW measurement given an already-pre-hashed data blob.
62///
63/// Computes `hash(data_hash ++ salt)` as the starting point, then runs the
64/// 5-round chained-hash algorithm.  Use `pow_compute_data_hash` to produce
65/// `data_hash` from raw inputs.
66pub fn pow_measure_from_data_hash(data_hash: &Hash, salt: &Salt) -> anyhow::Result<(Pow, Hash)> {
67    let mut data_current = hashing::hash_two(data_hash.as_ref(), salt.as_ref());
68
69    const CHAIN_LENGTH: usize = 5;
70    const MAX_REPETITIONS: usize = 2;
71
72    for _ in 0..CHAIN_LENGTH {
73        let algo_index = data_current.as_bytes()[0] as usize;
74        let repetitions = data_current.as_bytes()[1] as usize % MAX_REPETITIONS;
75
76        for _ in 0..=repetitions {
77            data_current = apply_chained_hash(algo_index, data_current)?;
78        }
79    }
80
81    let leading_zero_bits = tools::count_leading_zero_bits(data_current.as_bytes());
82    Ok((Pow(leading_zero_bits), data_current))
83}
84
85pub fn pow_measure(datas: &[&[u8]], salt: &Salt) -> anyhow::Result<(Pow, Hash)> {
86    pow_measure_from_data_hash(&pow_compute_data_hash(datas), salt)
87}
88
89#[cfg(test)]
90mod tests {
91    use crate::tools::pow::{pow_compute_data_hash, pow_measure, pow_measure_from_data_hash};
92    use crate::tools::tools;
93    use crate::tools::types::{Pow, Salt};
94
95    struct RegressionVector {
96        label: &'static str,
97        datas: Vec<Vec<u8>>,
98        salt_hex: &'static str,
99        expected_pow: u8,
100        expected_final_hash_hex: &'static str,
101    }
102
103    // Confirmed via `cargo llvm-cov nextest -p hashiverse-lib --tests pow::tests` that running
104    // `pow_regression_vectors_match` over these vectors hits every arm of the `apply_chained_hash`
105    // match block in pow.rs, so all 17 hash functions are exercised for regression.
106    fn regression_vectors() -> Vec<RegressionVector> {
107        vec![
108            RegressionVector {
109                label: "empty_data_zero_salt",
110                datas: vec![hex::decode("").unwrap()],
111                salt_hex: "0000000000000000",
112                expected_pow: 1,
113                expected_final_hash_hex: "6eec432cb487409c9500776340420e6281ac4aa06c2b7ec39916828dbf8bb39e",
114            },
115            RegressionVector {
116                label: "single_byte_zero_data",
117                datas: vec![hex::decode("00").unwrap()],
118                salt_hex: "0000000000000001",
119                expected_pow: 4,
120                expected_final_hash_hex: "0db42c05e85a32ac76f14c4a3132e8b82c0f97ba49e5bf0464173067ec20e86d",
121            },
122            RegressionVector {
123                label: "single_byte_high",
124                datas: vec![hex::decode("ff").unwrap()],
125                salt_hex: "ffffffffffffffff",
126                expected_pow: 0,
127                expected_final_hash_hex: "8b3754ac665ff1cdff1539552511b23b320c9865f0989add3bcaa245798be5a8",
128            },
129            RegressionVector {
130                label: "ascii_short",
131                datas: vec![hex::decode("68617368697665727365").unwrap()],
132                salt_hex: "0123456789abcdef",
133                expected_pow: 0,
134                expected_final_hash_hex: "adb2ef187879beb0615fc054ef2c94fcda7f022226b5f86d92485c00161bf081",
135            },
136            RegressionVector {
137                label: "ascii_long",
138                datas: vec![hex::decode("54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67").unwrap()],
139                salt_hex: "deadbeefcafebabe",
140                expected_pow: 0,
141                expected_final_hash_hex: "9502fa8669c3a191e3c0f3a59ce5b630c97c0460262cc8235b225ec88f05b27c",
142            },
143            RegressionVector {
144                label: "two_chunks_ascii",
145                datas: vec![hex::decode("68656c6c6f").unwrap(), hex::decode("776f726c64").unwrap()],
146                salt_hex: "fedcba9876543210",
147                expected_pow: 0,
148                expected_final_hash_hex: "8885e4616f1b5657c954a587a260c05d820c028fcc792ca27741d943507c397d",
149            },
150            RegressionVector {
151                label: "three_chunks_mixed",
152                datas: vec![hex::decode("61").unwrap(), hex::decode("6262").unwrap(), hex::decode("636363").unwrap()],
153                salt_hex: "1111111111111111",
154                expected_pow: 2,
155                expected_final_hash_hex: "2ca9bb4af0b9946d7b4ec2591f80148235355c75d4a6bb808257980c2546f6a3",
156            },
157            RegressionVector {
158                label: "binary_pattern",
159                datas: vec![hex::decode("0001020304050607").unwrap()],
160                salt_hex: "8000000000000000",
161                expected_pow: 3,
162                expected_final_hash_hex: "1080bab28b0963e88416512f313172774a0b1e538912928ed870f28c402d2e29",
163            },
164            RegressionVector {
165                label: "256_byte_zeroes",
166                datas: vec![hex::decode("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap()],
167                salt_hex: "a5a5a5a5a5a5a5a5",
168                expected_pow: 2,
169                expected_final_hash_hex: "39c0c67e6d471fee89b36f484237af21a83e2ab5113ff0c7a2adc66ce95a7de9",
170            },
171            RegressionVector {
172                label: "256_byte_ones",
173                datas: vec![hex::decode("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap()],
174                salt_hex: "5a5a5a5a5a5a5a5a",
175                expected_pow: 0,
176                expected_final_hash_hex: "eb532e53c96611844a9d7cb3417740572f7455a54f5a9c77dec08d24eccb3b26",
177            },
178        ]
179    }
180
181    #[tokio::test]
182    async fn pow_test() {
183        for _ in 1..1000 {
184            let mut data1 = [0u8; 1024];
185            tools::random_fill_bytes(&mut data1);
186            let mut data2 = [0u8; 512];
187            tools::random_fill_bytes(&mut data2);
188
189            let salt = Salt::random();
190            let _pow = pow_measure(&[&data1, &data2], &salt);
191        }
192    }
193
194    /// `pow_measure` must produce the same result as pre-hashing then calling
195    /// `pow_measure_from_data_hash` — the two-step path used by parallel workers.
196    #[tokio::test]
197    async fn pow_measure_and_from_data_hash_agree() -> anyhow::Result<()> {
198        for _ in 0..200 {
199            let mut data1 = [0u8; 256];
200            tools::random_fill_bytes(&mut data1);
201            let mut data2 = [0u8; 128];
202            tools::random_fill_bytes(&mut data2);
203            let salt = Salt::random();
204
205            let (pow_direct, hash_direct) = pow_measure(&[&data1, &data2], &salt)?;
206            let data_hash = pow_compute_data_hash(&[&data1, &data2]);
207            let (pow_split, hash_split) = pow_measure_from_data_hash(&data_hash, &salt)?;
208
209            assert_eq!(pow_direct, pow_split);
210            assert_eq!(hash_direct, hash_split);
211        }
212        Ok(())
213    }
214
215    /// Regression guard: a future dep bump that silently changes the output of any of the 17
216    /// chained hash algorithms (or of the blake3 pre-hash / hash-two step) will fail this test.
217    ///
218    /// Hard-coded vectors are captured against the current crate versions. When an *intentional*
219    /// future bump changes output, regenerate via the `pow_regression_vectors_print` helper.
220    #[test]
221    fn pow_regression_vectors_match() -> anyhow::Result<()> {
222        for vector in regression_vectors() {
223            let salt_bytes = hex::decode(vector.salt_hex).expect("salt_hex must be valid hex");
224            let salt = Salt::from_slice(&salt_bytes)?;
225            let data_refs: Vec<&[u8]> = vector.datas.iter().map(|v| v.as_slice()).collect();
226
227            let (pow_direct, hash_direct) = pow_measure(&data_refs, &salt)?;
228            let data_hash = pow_compute_data_hash(&data_refs);
229            let (pow_split, hash_split) = pow_measure_from_data_hash(&data_hash, &salt)?;
230
231            assert_eq!(pow_direct, pow_split, "vector {}: two-path mismatch", vector.label);
232            assert_eq!(hash_direct, hash_split, "vector {}: two-path mismatch", vector.label);
233            assert_eq!(pow_direct, Pow(vector.expected_pow), "vector {}: pow drift (likely crypto crate output change)", vector.label);
234            assert_eq!(hex::encode(hash_direct.as_bytes()), vector.expected_final_hash_hex, "vector {}: final-hash drift (likely crypto crate output change)", vector.label);
235        }
236        Ok(())
237    }
238
239
240    /*
241    /// Run with: `cargo nextest run -p hashiverse-lib pow::tests::pow_regression_vectors_print --run-ignored ignored-only --no-capture`
242    /// then paste the printed RegressionVector blocks back into `regression_vectors()`.
243    #[test]
244    #[ignore]
245    fn pow_regression_vectors_print() -> anyhow::Result<()> {
246        println!();
247        println!("// --- begin regenerated PoW regression vectors ---");
248        for vector in regression_vectors() {
249            let salt_bytes = hex::decode(vector.salt_hex).expect("salt_hex must be valid hex");
250            let salt = Salt::from_slice(&salt_bytes)?;
251            let data_refs: Vec<&[u8]> = vector.datas.iter().map(|v| v.as_slice()).collect();
252
253            let (pow, hash) = pow_measure(&data_refs, &salt)?;
254
255            let datas_literal = vector
256                .datas
257                .iter()
258                .map(|d| format!("hex::decode(\"{}\").unwrap()", hex::encode(d)))
259                .collect::<Vec<_>>()
260                .join(", ");
261
262            println!("RegressionVector {{");
263            println!("    label: \"{}\",", vector.label);
264            println!("    datas: vec![{}],", datas_literal);
265            println!("    salt_hex: \"{}\",", vector.salt_hex);
266            println!("    expected_pow: {},", pow.0);
267            println!("    expected_final_hash_hex: \"{}\",", hex::encode(hash.as_bytes()));
268            println!("}},");
269        }
270        println!("// --- end regenerated PoW regression vectors ---");
271        Ok(())
272    }
273    */
274}