use argon2::{Algorithm, Argon2, Version};
use ripemd::Ripemd320;
use serde::Serialize;
use sha2::{Digest, Sha256, Sha512};
pub use argon2::Params as Argon2Params;
pub use scrypt::Params as ScryptParams;
pub mod kpow;
pub mod bench {
use super::{meets_leading_zero_bits, Argon2Params, PoWAlgorithm};
use hex::encode as hex_encode;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::time::Instant;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BenchOutcome {
pub bits: u32,
pub data_len: usize,
pub time_ms: u128,
pub tries: u64,
pub nonce: u64,
pub hash_hex: String,
pub m_kib: u32,
pub t_cost: u32,
pub p_cost: u32,
}
const Z95: f64 = 1.959963984540054;
const Z99: f64 = 2.5758293035489004;
fn confidence_bounds(mean: f64, stderr: f64) -> (f64, f64, f64, f64) {
if stderr <= f64::EPSILON {
return (mean, mean, mean, mean);
}
let ci95_low = mean - Z95 * stderr;
let ci95_high = mean + Z95 * stderr;
let ci99_low = mean - Z99 * stderr;
let ci99_high = mean + Z99 * stderr;
(ci95_low, ci95_high, ci99_low, ci99_high)
}
#[allow(dead_code)]
pub fn argon2_params_kib(
memory_cost_kib: u32,
time_cost: u32,
parallelism: u32,
) -> Result<Argon2Params, String> {
Argon2Params::new(memory_cost_kib, time_cost, parallelism, None)
.map_err(|err| err.to_string())
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BenchSummary {
pub mean_time_ms: f64,
pub std_time_ms: f64,
pub min_time_ms: u128,
pub max_time_ms: u128,
pub stderr_time_ms: f64,
pub ci95_low_time_ms: f64,
pub ci95_high_time_ms: f64,
pub ci99_low_time_ms: f64,
pub ci99_high_time_ms: f64,
pub mean_tries: f64,
pub std_tries: f64,
pub min_tries: u64,
pub max_tries: u64,
pub stderr_tries: f64,
pub ci95_low_tries: f64,
pub ci95_high_tries: f64,
pub ci99_low_tries: f64,
pub ci99_high_tries: f64,
}
#[allow(dead_code)]
pub fn summarize(outcomes: &[BenchOutcome]) -> Result<BenchSummary, String> {
if outcomes.is_empty() {
return Err("no outcomes supplied".to_owned());
}
let count = outcomes.len() as f64;
let mut sum_time = 0.0f64;
let mut sum_time_sq = 0.0f64;
let mut sum_tries = 0.0f64;
let mut sum_tries_sq = 0.0f64;
let mut min_time = u128::MAX;
let mut max_time = u128::MIN;
let mut min_tries = u64::MAX;
let mut max_tries = u64::MIN;
for outcome in outcomes {
let time = outcome.time_ms as f64;
let tries = outcome.tries as f64;
sum_time += time;
sum_time_sq += time * time;
sum_tries += tries;
sum_tries_sq += tries * tries;
min_time = min_time.min(outcome.time_ms);
max_time = max_time.max(outcome.time_ms);
min_tries = min_tries.min(outcome.tries);
max_tries = max_tries.max(outcome.tries);
}
let mean_time_ms = sum_time / count;
let n = outcomes.len() as f64;
let std_time_ms = if outcomes.len() > 1 {
let variance_time = (sum_time_sq - (sum_time * sum_time) / n) / (n - 1.0);
variance_time.max(0.0).sqrt()
} else {
0.0
};
let stderr_time_ms = if n > 0.0 { std_time_ms / n.sqrt() } else { 0.0 };
let (ci95_low_time_ms, ci95_high_time_ms, ci99_low_time_ms, ci99_high_time_ms) =
confidence_bounds(mean_time_ms, stderr_time_ms);
let mean_tries = sum_tries / count;
let std_tries = if outcomes.len() > 1 {
let variance_tries = (sum_tries_sq - (sum_tries * sum_tries) / n) / (n - 1.0);
variance_tries.max(0.0).sqrt()
} else {
0.0
};
let stderr_tries = if n > 0.0 { std_tries / n.sqrt() } else { 0.0 };
let (ci95_low_tries, ci95_high_tries, ci99_low_tries, ci99_high_tries) =
confidence_bounds(mean_tries, stderr_tries);
Ok(BenchSummary {
mean_time_ms,
std_time_ms,
min_time_ms: min_time,
max_time_ms: max_time,
stderr_time_ms,
ci95_low_time_ms,
ci95_high_time_ms,
ci99_low_time_ms,
ci99_high_time_ms,
mean_tries,
std_tries,
min_tries,
max_tries,
stderr_tries,
ci95_low_tries,
ci95_high_tries,
ci99_low_tries,
ci99_high_tries,
})
}
#[allow(dead_code)]
pub const fn csv_header() -> &'static str {
"kind,algo,mode,m_kib,t_cost,p_cost,data_len,bits,run_idx,time_ms,tries,nonce,hash_hex,mean_time_ms,std_time_ms,stderr_time_ms,ci95_low_time_ms,ci95_high_time_ms,ci99_low_time_ms,ci99_high_time_ms,min_time_ms,max_time_ms,mean_tries,std_tries,stderr_tries,ci95_low_tries,ci95_high_tries,ci99_low_tries,ci99_high_tries,min_tries,max_tries"
}
#[allow(dead_code)]
pub fn csv_row_run(outcome: &BenchOutcome, algo: &str, mode: &str, run_idx: u32) -> String {
format!(
"run,{algo},{mode},{},{},{},{},{},{},{},{},{},{},,,,,,,,",
outcome.m_kib,
outcome.t_cost,
outcome.p_cost,
outcome.data_len,
outcome.bits,
run_idx,
outcome.time_ms,
outcome.tries,
outcome.nonce,
outcome.hash_hex
)
}
#[allow(dead_code)]
#[allow(clippy::too_many_arguments)]
pub fn csv_row_summary(
bits: u32,
data_len: usize,
m_kib: u32,
t_cost: u32,
p_cost: u32,
summary: &BenchSummary,
algo: &str,
mode: &str,
) -> String {
format!(
"summary,{algo},{mode},{m_kib},{t_cost},{p_cost},{data_len},{bits},,,,,,{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{},{},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{},{}",
summary.mean_time_ms,
summary.std_time_ms,
summary.stderr_time_ms,
summary.ci95_low_time_ms,
summary.ci95_high_time_ms,
summary.ci99_low_time_ms,
summary.ci99_high_time_ms,
summary.min_time_ms,
summary.max_time_ms,
summary.mean_tries,
summary.std_tries,
summary.stderr_tries,
summary.ci95_low_tries,
summary.ci95_high_tries,
summary.ci99_low_tries,
summary.ci99_high_tries,
summary.min_tries,
summary.max_tries
)
}
#[allow(dead_code)]
pub fn bench_argon2_leading_bits_once(
data: &[u8],
bits: u32,
params: &Argon2Params,
start_nonce: u64,
) -> Result<BenchOutcome, String> {
let mut nonce = start_nonce;
let mut tries: u64 = 0;
let algorithm = PoWAlgorithm::Argon2id(params.clone());
let start = Instant::now();
loop {
let nonce_usize = usize::try_from(nonce)
.map_err(|_| "nonce exceeds usize::MAX on this platform".to_owned())?;
let hash = algorithm.calculate(data, nonce_usize);
tries = tries
.checked_add(1)
.ok_or_else(|| "tries overflow".to_owned())?;
if meets_leading_zero_bits(&hash, bits) {
let time_ms = start.elapsed().as_millis();
return Ok(BenchOutcome {
bits,
data_len: data.len(),
time_ms,
tries,
nonce,
hash_hex: hex_encode(hash),
m_kib: params.m_cost(),
t_cost: params.t_cost(),
p_cost: params.p_cost(),
});
}
nonce = nonce
.checked_add(1)
.ok_or_else(|| "nonce overflow".to_owned())?;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn summarize_basic_stats() {
let outcomes = [
BenchOutcome {
bits: 1,
data_len: 4,
time_ms: 10,
tries: 2,
nonce: 5,
hash_hex: String::new(),
m_kib: 8,
t_cost: 1,
p_cost: 1,
},
BenchOutcome {
bits: 1,
data_len: 4,
time_ms: 20,
tries: 4,
nonce: 6,
hash_hex: String::new(),
m_kib: 8,
t_cost: 1,
p_cost: 1,
},
];
let summary = summarize(&outcomes).expect("summary");
assert_eq!(summary.min_time_ms, 10);
assert_eq!(summary.max_time_ms, 20);
assert_eq!(summary.min_tries, 2);
assert_eq!(summary.max_tries, 4);
assert!((summary.mean_time_ms - 15.0).abs() < f64::EPSILON);
assert!((summary.mean_tries - 3.0).abs() < f64::EPSILON);
assert!((summary.std_time_ms - 7.0710678118654755).abs() < 1e-9);
assert!((summary.stderr_time_ms - 5.0).abs() < 1e-9);
assert!((summary.ci95_low_time_ms - 5.200180077299731).abs() < 1e-6);
assert!((summary.ci95_high_time_ms - 24.79981992270027).abs() < 1e-6);
assert!((summary.ci99_low_time_ms - 2.120853482255498).abs() < 1e-6);
assert!((summary.ci99_high_time_ms - 27.879146517744502).abs() < 1e-6);
assert!((summary.std_tries - 1.4142135623730951).abs() < 1e-9);
assert!((summary.stderr_tries - 1.0).abs() < 1e-9);
assert!((summary.ci95_low_tries - 1.040036015459946).abs() < 1e-6);
assert!((summary.ci95_high_tries - 4.959963984540054).abs() < 1e-6);
assert!((summary.ci99_low_tries - 0.4241706964510996).abs() < 1e-6);
assert!((summary.ci99_high_tries - 5.5758293035489).abs() < 1e-6);
}
#[test]
fn summarize_single_sample() {
let outcomes = [BenchOutcome {
bits: 1,
data_len: 4,
time_ms: 42,
tries: 7,
nonce: 10,
hash_hex: String::new(),
m_kib: 8,
t_cost: 1,
p_cost: 1,
}];
let summary = summarize(&outcomes).expect("summary");
assert_eq!(summary.mean_time_ms, 42.0);
assert_eq!(summary.std_time_ms, 0.0);
assert_eq!(summary.stderr_time_ms, 0.0);
assert_eq!(summary.ci95_low_time_ms, 42.0);
assert_eq!(summary.ci95_high_time_ms, 42.0);
assert_eq!(summary.ci99_low_time_ms, 42.0);
assert_eq!(summary.ci99_high_time_ms, 42.0);
assert_eq!(summary.mean_tries, 7.0);
assert_eq!(summary.std_tries, 0.0);
assert_eq!(summary.stderr_tries, 0.0);
}
#[test]
fn csv_alignment_summary_columns() {
let header = csv_header();
let h: Vec<&str> = header.split(',').collect();
let outcomes = [
BenchOutcome {
bits: 1,
data_len: 4,
time_ms: 10,
tries: 2,
nonce: 0,
hash_hex: String::new(),
m_kib: 8,
t_cost: 1,
p_cost: 1,
},
BenchOutcome {
bits: 1,
data_len: 4,
time_ms: 20,
tries: 4,
nonce: 0,
hash_hex: String::new(),
m_kib: 8,
t_cost: 1,
p_cost: 1,
},
];
let s = summarize(&outcomes).unwrap();
let row = csv_row_summary(1, 4, 8, 1, 1, &s, "argon2id", "leading_zero_bits");
let c: Vec<&str> = row.split(',').collect();
assert_eq!(h.len(), c.len(), "header/row column count mismatch");
let ci99_low_time: f64 = c[18].parse().unwrap();
let ci99_high_time: f64 = c[19].parse().unwrap();
assert!(ci99_low_time <= ci99_high_time);
let ci99_low_tries: f64 = c[27].parse().unwrap();
let ci99_high_tries: f64 = c[28].parse().unwrap();
assert!(ci99_low_tries <= ci99_high_tries);
}
}
}
#[allow(non_camel_case_types)]
pub enum PoWAlgorithm {
Sha2_256,
Sha2_512,
RIPEMD_320,
Scrypt(ScryptParams),
Argon2id(Argon2Params),
}
impl PoWAlgorithm {
pub fn calculate_sha2_256(data: &[u8], nonce: usize) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.update(nonce.to_le_bytes());
let final_hash = hasher.finalize();
final_hash.to_vec()
}
pub fn calculate_sha2_512(data: &[u8], nonce: usize) -> Vec<u8> {
let mut hasher = Sha512::new();
hasher.update(data);
hasher.update(nonce.to_le_bytes());
let final_hash = hasher.finalize();
final_hash.to_vec()
}
pub fn calculate_ripemd_320(data: &[u8], nonce: usize) -> Vec<u8> {
let mut hasher = Ripemd320::new();
hasher.update(data);
hasher.update(nonce.to_le_bytes());
let final_hash = hasher.finalize();
final_hash.to_vec()
}
pub fn calculate_scrypt(data: &[u8], nonce: usize, params: &ScryptParams) -> Vec<u8> {
let mut output = vec![0; 32];
let _ = scrypt::scrypt(data, &nonce.to_le_bytes(), params, &mut output);
output
}
pub fn calculate_argon2id(data: &[u8], nonce: usize, params: &Argon2Params) -> Vec<u8> {
let mut output = vec![0; 32];
let a2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params.to_owned());
a2.hash_password_into(data, &nonce.to_le_bytes(), &mut output)
.unwrap();
output
}
pub fn calculate(&self, data: &[u8], nonce: usize) -> Vec<u8> {
match self {
Self::Sha2_256 => Self::calculate_sha2_256(data, nonce),
Self::Sha2_512 => Self::calculate_sha2_512(data, nonce),
Self::RIPEMD_320 => Self::calculate_ripemd_320(data, nonce),
Self::Scrypt(params) => Self::calculate_scrypt(data, nonce, params),
Self::Argon2id(params) => Self::calculate_argon2id(data, nonce, params),
}
}
}
pub fn meets_leading_zero_bits(hash: &[u8], bits: u32) -> bool {
if bits == 0 {
return true;
}
let total_bits = (hash.len() as u32) * 8;
if bits > total_bits {
return false;
}
let full_bytes = (bits / 8) as usize;
let rem_bits = (bits % 8) as u8;
for b in hash.iter().take(full_bytes) {
if *b != 0 {
return false;
}
}
if rem_bits > 0 {
let b = hash[full_bytes];
let mask = 0xFFu8 << (8 - rem_bits);
if (b & mask) != 0 {
return false;
}
}
true
}
#[derive(Clone, Copy)]
pub enum DifficultyMode {
AsciiZeroPrefix,
LeadingZeroBits,
}
pub struct PoW {
data: Vec<u8>,
difficulty: usize,
algorithm: PoWAlgorithm,
mode: DifficultyMode,
}
impl PoW {
pub fn new(
data: impl Serialize,
difficulty: usize,
algorithm: PoWAlgorithm,
) -> Result<Self, String> {
Ok(PoW {
data: serde_json::to_vec(&data).unwrap(),
difficulty,
algorithm,
mode: DifficultyMode::AsciiZeroPrefix,
})
}
pub fn with_mode(
data: impl Serialize,
difficulty: usize,
algorithm: PoWAlgorithm,
mode: DifficultyMode,
) -> Result<Self, String> {
Ok(PoW {
data: serde_json::to_vec(&data).unwrap(),
difficulty,
algorithm,
mode,
})
}
pub fn calculate_target(&self) -> Vec<u8> {
vec![0x30u8; self.difficulty]
}
pub fn calculate_pow(&self, target: &[u8]) -> (Vec<u8>, usize) {
let mut nonce = 0;
loop {
let hash = self.algorithm.calculate(&self.data, nonce);
match self.mode {
DifficultyMode::AsciiZeroPrefix => {
if &hash[..target.len()] == target {
return (hash, nonce);
}
}
DifficultyMode::LeadingZeroBits => {
if meets_leading_zero_bits(&hash, self.difficulty as u32) {
return (hash, nonce);
}
}
}
nonce += 1;
}
}
pub fn verify_pow(&self, target: &[u8], pow_result: (Vec<u8>, usize)) -> bool {
let (hash, nonce) = pow_result;
let calculated_hash = self.algorithm.calculate(&self.data, nonce);
match self.mode {
DifficultyMode::AsciiZeroPrefix => {
if &calculated_hash[..target.len()] == target && calculated_hash == hash {
return true;
}
false
}
DifficultyMode::LeadingZeroBits => {
if meets_leading_zero_bits(&calculated_hash, self.difficulty as u32)
&& calculated_hash == hash
{
return true;
}
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pow_algorithm_sha2_256() {
let data = b"hello world";
let nonce = 12345;
let expected_hash = [
113, 212, 92, 254, 42, 99, 0, 112, 60, 9, 31, 138, 105, 191, 234, 231, 122, 30, 73, 12,
3, 10, 182, 230, 134, 80, 94, 32, 162, 164, 204, 9,
];
let hash = PoWAlgorithm::calculate_sha2_256(data, nonce);
assert_eq!(hash, expected_hash);
}
#[test]
fn test_pow_algorithm_sha2_512() {
let data = b"hello world";
let nonce = 12345;
let expected_hash = [
166, 65, 125, 254, 189, 250, 254, 9, 146, 145, 86, 129, 163, 210, 160, 17, 234, 234,
87, 92, 214, 37, 91, 204, 146, 93, 65, 135, 191, 41, 107, 117, 29, 81, 124, 53, 202,
89, 149, 159, 8, 113, 241, 163, 84, 231, 16, 32, 237, 17, 9, 182, 201, 68, 83, 241, 39,
23, 106, 152, 58, 110, 134, 144,
];
let hash = PoWAlgorithm::calculate_sha2_512(data, nonce);
assert_eq!(hash, expected_hash);
}
#[test]
fn test_pow_algorithm_ripemd_320() {
let data = b"hello world";
let nonce = 12345;
let expected_hash = [
136, 243, 131, 91, 134, 239, 75, 101, 140, 4, 66, 6, 143, 87, 176, 118, 94, 92, 142,
211, 74, 63, 182, 20, 119, 221, 125, 126, 20, 227, 45, 10, 34, 110, 210, 133, 131, 44,
45, 23,
];
let hash = PoWAlgorithm::calculate_ripemd_320(data, nonce);
assert_eq!(hash, expected_hash);
}
#[test]
fn test_pow_algorithm_dispatch_ripemd_320() {
let data = b"hello world";
let nonce = 12345;
let via_dispatch = PoWAlgorithm::RIPEMD_320.calculate(data, nonce);
let direct = PoWAlgorithm::calculate_ripemd_320(data, nonce);
assert_eq!(via_dispatch, direct);
}
#[test]
fn test_pow_algorithm_scrypt() {
let data = b"hello world";
let nonce = 12345;
let params = ScryptParams::new(8, 4, 1, 32).unwrap();
let expected_hash = [
214, 100, 105, 187, 137, 13, 176, 155, 184, 158, 6, 229, 136, 55, 197, 78, 159, 216,
153, 53, 214, 163, 145, 214, 252, 84, 4, 185, 92, 91, 111, 234,
];
let hash = PoWAlgorithm::calculate_scrypt(data, nonce, ¶ms);
assert_eq!(hash, expected_hash);
}
#[test]
fn test_pow_algorithm_argon2id() {
let data = b"hello world";
let nonce = 12345;
let params = Argon2Params::new(16, 2, 2, None).unwrap();
let expected_hash = [
243, 150, 29, 238, 126, 244, 47, 122, 69, 22, 69, 20, 102, 5, 218, 124, 251, 140, 204,
53, 133, 2, 147, 207, 66, 17, 241, 177, 20, 249, 251, 155,
];
let hash = PoWAlgorithm::calculate_argon2id(data, nonce, ¶ms);
assert_eq!(hash, expected_hash);
}
#[test]
fn test_pow_calculate_pow() {
let data = "hello world";
let difficulty = 2;
let target = "00".as_bytes();
let algorithm = PoWAlgorithm::Sha2_512;
let pow = PoW::new(data, difficulty, algorithm).unwrap();
let (hash, nonce) = pow.calculate_pow(&target);
assert!(hash.starts_with(&target[..difficulty]));
assert!(pow.verify_pow(&target, (hash.clone(), nonce)));
}
#[test]
fn test_pow_calculate_pow_leading_zero_bits() {
let data = "hello world";
let bits = 8; let algorithm = PoWAlgorithm::Sha2_256;
let pow = PoW::with_mode(data, bits, algorithm, DifficultyMode::LeadingZeroBits).unwrap();
let (hash, nonce) = pow.calculate_pow(&[]);
assert!(meets_leading_zero_bits(&hash, bits as u32));
assert!(pow.verify_pow(&[], (hash, nonce)));
}
#[test]
fn test_meets_leading_zero_bits_basic() {
let h = [0x00u8, 0x00u8, 0xFFu8];
assert!(meets_leading_zero_bits(&h, 0));
assert!(meets_leading_zero_bits(&h, 1));
assert!(meets_leading_zero_bits(&h, 7));
assert!(meets_leading_zero_bits(&h, 8));
assert!(meets_leading_zero_bits(&h, 9));
assert!(meets_leading_zero_bits(&h, 15));
assert!(meets_leading_zero_bits(&h, 16));
assert!(!meets_leading_zero_bits(&h, 17));
}
#[test]
fn test_meets_leading_zero_bits_edges() {
let h1 = [0x80u8, 0x00u8];
assert!(!meets_leading_zero_bits(&h1, 1));
let h2 = [0x7Fu8, 0xFFu8];
assert!(meets_leading_zero_bits(&h2, 1));
assert!(!meets_leading_zero_bits(&h2, 2));
let h3 = [0x00u8, 0x80u8];
assert!(meets_leading_zero_bits(&h3, 8));
assert!(!meets_leading_zero_bits(&h3, 9));
let h4 = [0x00u8, 0x00u8, 0x00u8];
assert!(meets_leading_zero_bits(&h4, 24));
assert!(!meets_leading_zero_bits(&h4, 25));
}
}