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::pow_required_estimator::PowRequiredEstimator;
6use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
7use crate::tools::types::{Hash, Pow, Salt};
8use crate::tools::{hashing, tools};
9use digest::consts::{U32, U64};
10
11use digest::generic_array::GenericArray;
12use digest::Digest;
13use log::trace;
14
15fn apply_hash<H>(data: &Hash) -> anyhow::Result<Hash>
16where H: Digest
17{
18    Hash::from_slice(&H::digest(data.as_ref()).as_slice()[0..32])
19}
20
21fn apply_chained_hash(algo_index: usize, hash_current: Hash) -> anyhow::Result<Hash> {
22
23    const ALGO_COUNT: usize = 17;
24    let algo_index = algo_index % ALGO_COUNT;
25
26    match algo_index {
27        // --- Cases 0-13: Use the existing clean apply_hash ---
28        0 => apply_hash::<blake2::Blake2s256>(&hash_current),
29        1 => apply_hash::<blake2::Blake2b512>(&hash_current),
30        2 => apply_hash::<sha2::Sha256>(&hash_current),
31        3 => apply_hash::<sha2::Sha384>(&hash_current),
32        4 => apply_hash::<sha2::Sha512>(&hash_current),
33        5 => apply_hash::<sha3::Sha3_256>(&hash_current),
34        6 => apply_hash::<sha3::Sha3_384>(&hash_current),
35        7 => apply_hash::<sha3::Sha3_512>(&hash_current),
36        8 => apply_hash::<sha3::Keccak256>(&hash_current),
37        9 => apply_hash::<sha3::Keccak384>(&hash_current),
38        10 => apply_hash::<sha3::Keccak512>(&hash_current),
39        11 => apply_hash::<groestl::Groestl256>(&hash_current),
40        12 => apply_hash::<groestl::Groestl512>(&hash_current),
41        13 => apply_hash::<whirlpool::Whirlpool>(&hash_current),
42
43        // --- Cases 14-16: Keep custom logic here ---
44        14 => {
45            let mut hasher = skein::Skein256::new();
46            hasher.update(hash_current.as_ref());
47            let hash_output: GenericArray<u8, U32> = hasher.finalize();
48            Hash::from_slice(&hash_output.as_slice()[0..32])
49        },
50        15 => {
51            let mut hasher = skein::Skein512::new();
52            hasher.update(hash_current.as_ref());
53            let hash_output: GenericArray<u8, U64> = hasher.finalize();
54            Hash::from_slice(&hash_output.as_slice()[0..32])
55        },
56        16 => {
57            let mut hasher = blake3::Hasher::new();
58            hasher.update(hash_current.as_ref());
59            let hash_output = hasher.finalize();
60            Hash::from_slice(&hash_output.as_bytes()[0..32])
61        },
62
63        _ => Ok(hash_current),
64    }
65}
66
67/// Pre-hash all input data into a single 32-byte `Hash`.
68///
69/// Call this once before the iteration loop; pass the result to
70/// `pow_measure_from_data_hash` / `pow_generate_with_iteration_limit` so that
71/// workers only receive 32 bytes instead of the full raw data.
72pub fn pow_compute_data_hash(datas: &[&[u8]]) -> Hash {
73    hashing::hash_multiple(datas)
74}
75
76/// Core PoW measurement given an already-pre-hashed data blob.
77///
78/// Computes `hash(data_hash ++ salt)` as the starting point, then runs the
79/// 5-round chained-hash algorithm.  Use `pow_compute_data_hash` to produce
80/// `data_hash` from raw inputs.
81pub fn pow_measure_from_data_hash(data_hash: &Hash, salt: &Salt) -> anyhow::Result<(Pow, Hash)> {
82    let mut data_current = hashing::hash_two(data_hash.as_ref(), salt.as_ref());
83
84    const CHAIN_LENGTH: usize = 5;
85    const MAX_REPETITIONS: usize = 2;
86
87    for _ in 0..CHAIN_LENGTH {
88        let algo_index = data_current.as_bytes()[0] as usize;
89        let repetitions = data_current.as_bytes()[1] as usize % MAX_REPETITIONS;
90
91        for _ in 0..=repetitions {
92            data_current = apply_chained_hash(algo_index, data_current)?;
93        }
94    }
95
96    let leading_zero_bits = tools::count_leading_zero_bits(data_current.as_bytes());
97    Ok((Pow(leading_zero_bits), data_current))
98}
99
100pub fn pow_measure(datas: &[&[u8]], salt: &Salt) -> anyhow::Result<(Pow, Hash)> {
101    pow_measure_from_data_hash(&pow_compute_data_hash(datas), salt)
102}
103
104/// Try find a sufficient PoW.
105///
106/// It will return the best so far if is it unsuccessful after the `iteration_limit`
107pub async fn pow_generate_with_iteration_limit(iteration_limit: usize, pow_min: Pow, data_hash: &Hash) -> anyhow::Result<(Salt, Pow, Hash)> {
108    let mut salt: Salt;
109    let mut pow_best_so_far = (Salt::zero(), Pow(0), Hash::zero());
110
111    for _ in 0..iteration_limit {
112        salt = Salt::random();
113        let (pow, hash) = pow_measure_from_data_hash(data_hash, &salt)?;
114
115        if pow >= pow_min {
116            return Ok((salt, pow, hash));
117        }
118
119        if pow > pow_best_so_far.1 {
120            pow_best_so_far = (salt, pow, hash);
121        }
122    }
123
124    Ok(pow_best_so_far)
125}
126
127/// Try forever until a valid PoW is found.
128///
129/// This method "yields" occasionally so that other tokio processes can make progress.
130pub async fn pow_generate(pow_required: Pow, datas: &[&[u8]]) -> anyhow::Result<(Salt, Pow, Hash)> {
131    const BATCH_SIZE: usize = 64 * 1024;
132    let real_time_provider = RealTimeProvider::default();
133    let mut estimator = PowRequiredEstimator::new(real_time_provider.current_time_millis(), "pow_generate", pow_required);
134    let data_hash = pow_compute_data_hash(datas);
135    loop {
136        let result = pow_generate_with_iteration_limit(BATCH_SIZE, pow_required, &data_hash).await?;
137        if result.1 >= pow_required {
138            return Ok(result);
139        }
140
141        let progress = estimator.record_batch_and_estimate(real_time_provider.current_time_millis(), BATCH_SIZE, result.1);
142        trace!("{}", progress);
143        tools::yield_now().await;
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use crate::tools::pow::{pow_compute_data_hash, pow_generate, pow_measure, pow_measure_from_data_hash};
150    use crate::tools::tools;
151    use crate::tools::types::{Pow, Salt};
152
153    #[tokio::test]
154    async fn pow_test() {
155        for _ in 1..1000 {
156            let mut data1 = [0u8; 1024];
157            tools::random_fill_bytes(&mut data1);
158            let mut data2 = [0u8; 512];
159            tools::random_fill_bytes(&mut data2);
160
161            let salt = Salt::random();
162            let _pow = pow_measure(&[&data1, &data2], &salt);
163        }
164    }
165
166    #[tokio::test]
167    async fn pow_generate_test() -> anyhow::Result<()> {
168        const POW_MIN: Pow = Pow(16);
169
170        let mut data = [0u8; 1024];
171        tools::random_fill_bytes(&mut data);
172        let (salt, _, _) = pow_generate(POW_MIN, &[&data]).await?;
173        let (pow, _) = pow_measure(&[&data], &salt)?;
174        assert!(pow >= POW_MIN);
175
176        Ok(())
177    }
178
179    /// `pow_measure` must produce the same result as pre-hashing then calling
180    /// `pow_measure_from_data_hash` — the two-step path used by parallel workers.
181    #[tokio::test]
182    async fn pow_measure_and_from_data_hash_agree() -> anyhow::Result<()> {
183        for _ in 0..200 {
184            let mut data1 = [0u8; 256];
185            tools::random_fill_bytes(&mut data1);
186            let mut data2 = [0u8; 128];
187            tools::random_fill_bytes(&mut data2);
188            let salt = Salt::random();
189
190            let (pow_direct, hash_direct) = pow_measure(&[&data1, &data2], &salt)?;
191            let data_hash = pow_compute_data_hash(&[&data1, &data2]);
192            let (pow_split, hash_split) = pow_measure_from_data_hash(&data_hash, &salt)?;
193
194            assert_eq!(pow_direct, pow_split);
195            assert_eq!(hash_direct, hash_split);
196        }
197        Ok(())
198    }
199}