use std::sync::Arc;
use chrono::Utc;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use foundation_db::{AsyncKeyValueStore, KeyValueStore, StorageError};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PowChallenge {
pub difficulty: u32,
pub challenge: String,
pub expires_in: u64,
}
#[derive(Debug, Deserialize)]
pub struct PowSolution {
pub challenge: String,
pub solution: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChallengeEntry {
difficulty: u32,
expires_at: i64,
solved: bool,
}
fn challenge_key(challenge: &str) -> String {
format!("pow:challenge:{challenge}")
}
pub struct PowService<KV: KeyValueStore> {
cache: KV,
difficulty: u32,
validity_secs: u64,
solve_validity_secs: u64,
}
impl<KV: KeyValueStore> PowService<KV> {
#[must_use]
pub fn new(
cache: KV,
difficulty: u32,
challenge_validity_secs: u64,
solve_validity_secs: u64,
) -> Self {
Self {
cache,
difficulty,
validity_secs: challenge_validity_secs,
solve_validity_secs,
}
}
pub fn generate_challenge(&self) -> Result<PowChallenge, StorageError> {
let mut rng = rand::thread_rng();
let mut bytes = [0u8; 16];
rng.fill_bytes(&mut bytes);
let challenge = URL_SAFE_NO_PAD.encode(bytes);
let entry = ChallengeEntry {
difficulty: self.difficulty,
expires_at: Utc::now().timestamp() + self.validity_secs as i64,
solved: false,
};
self.cache.set(&challenge_key(&challenge), entry)?;
Ok(PowChallenge {
difficulty: self.difficulty,
challenge,
expires_in: self.validity_secs,
})
}
pub fn verify_solution(&self, solution: &PowSolution) -> bool {
let key = challenge_key(&solution.challenge);
let mut entry: ChallengeEntry = match self.cache.get(&key) {
Ok(Some(e)) => e,
Ok(None) => return false,
Err(e) => {
eprintln!("PoW: failed to read challenge from cache: {e}");
return false;
}
};
if Utc::now().timestamp() >= entry.expires_at {
let _ = self.cache.delete(&key); return false;
}
if entry.solved {
return false;
}
let input = format!("{}{}", solution.challenge, solution.solution);
let hash = Sha256::digest(input.as_bytes());
let valid = count_leading_zero_bytes(&hash) >= entry.difficulty / 8;
if valid {
entry.solved = true;
if let Err(e) = self.cache.set(&key, entry) {
eprintln!("cache error: {e}");
return false;
}
}
valid
}
}
pub struct AsyncPowService<AKV: AsyncKeyValueStore> {
cache: AKV,
difficulty: u32,
validity_secs: u64,
solve_validity_secs: u64,
}
impl<AKV: AsyncKeyValueStore> AsyncPowService<AKV> {
#[must_use]
pub fn new(
cache: AKV,
difficulty: u32,
challenge_validity_secs: u64,
solve_validity_secs: u64,
) -> Self {
Self {
cache,
difficulty,
validity_secs: challenge_validity_secs,
solve_validity_secs,
}
}
pub async fn generate_challenge(&self) -> Result<PowChallenge, StorageError> {
let mut rng = rand::thread_rng();
let mut bytes = [0u8; 16];
rng.fill_bytes(&mut bytes);
let challenge = URL_SAFE_NO_PAD.encode(bytes);
let entry = ChallengeEntry {
difficulty: self.difficulty,
expires_at: Utc::now().timestamp() + self.validity_secs as i64,
solved: false,
};
self.cache.set_async(&challenge_key(&challenge), entry).await?;
Ok(PowChallenge {
difficulty: self.difficulty,
challenge,
expires_in: self.validity_secs,
})
}
pub async fn verify_solution(&self, solution: &PowSolution) -> bool {
let key = challenge_key(&solution.challenge);
let mut entry: ChallengeEntry = match self.cache.get_async(&key).await {
Ok(Some(e)) => e,
Ok(None) => return false,
Err(e) => {
eprintln!("PoW: failed to read challenge from cache: {e}");
return false;
}
};
if Utc::now().timestamp() >= entry.expires_at {
let _ = self.cache.delete_async(&key).await; return false;
}
if entry.solved {
return false;
}
let input = format!("{}{}", solution.challenge, solution.solution);
let hash = Sha256::digest(input.as_bytes());
let valid = count_leading_zero_bytes(&hash) >= entry.difficulty / 8;
if valid {
entry.solved = true;
if let Err(e) = self.cache.set_async(&key, entry).await {
eprintln!("cache error: {e}");
return false;
}
}
valid
}
}
fn count_leading_zero_bytes(hash: &[u8]) -> u32 {
let mut count = 0;
for &byte in hash {
if byte == 0 {
count += 8;
} else {
count += byte.leading_zeros();
break;
}
}
count
}