pub const MAX_DIFFICULTY: u32 = 28;
const MAX_ITERATIONS: u64 = 1 << 30;
pub fn leading_zero_bits(digest: &[u8; 32]) -> u32 {
let mut bits = 0u32;
for &byte in digest {
if byte == 0 {
bits += 8;
} else {
bits += byte.leading_zeros();
break;
}
}
bits
}
pub fn solve(challenge: &str, difficulty: u32) -> Option<String> {
use sha2::{Digest, Sha256};
if difficulty > MAX_DIFFICULTY {
return None;
}
let prefix = challenge.as_bytes();
let mut n: u64 = 0;
while n < MAX_ITERATIONS {
let solution = n.to_string();
let mut hasher = Sha256::new();
hasher.update(prefix);
hasher.update(solution.as_bytes());
let digest: [u8; 32] = hasher.finalize().into();
if leading_zero_bits(&digest) >= difficulty {
return Some(solution);
}
n += 1;
}
None
}
pub async fn solve_async(challenge: String, difficulty: u32) -> Option<String> {
tokio::task::spawn_blocking(move || solve(&challenge, difficulty))
.await
.ok()
.flatten()
}
pub fn header_value(challenge: &str, solution: &str) -> String {
format!("{challenge}.{solution}")
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
fn lzb_of(challenge: &str, solution: &str) -> u32 {
let mut h = Sha256::new();
h.update(challenge.as_bytes());
h.update(solution.as_bytes());
let d: [u8; 32] = h.finalize().into();
leading_zero_bits(&d)
}
#[test]
fn leading_zero_bits_counts_correctly() {
let mut d = [0u8; 32];
assert_eq!(leading_zero_bits(&d), 256);
d[0] = 0x80; assert_eq!(leading_zero_bits(&d), 0);
d[0] = 0x01; assert_eq!(leading_zero_bits(&d), 7);
d[0] = 0x00;
d[1] = 0x40; assert_eq!(leading_zero_bits(&d), 9);
}
#[test]
fn solve_meets_difficulty() {
let challenge = "payload_b64.deadbeefmac";
let difficulty = 12;
let solution = solve(challenge, difficulty).expect("solvable at difficulty 12");
assert!(lzb_of(challenge, &solution) >= difficulty);
}
#[test]
fn solve_rejects_excessive_difficulty() {
assert!(solve("x.y", MAX_DIFFICULTY + 1).is_none());
}
#[test]
fn header_value_joins_with_dot() {
assert_eq!(header_value("a.b", "123"), "a.b.123");
}
}