use serde::Serialize;
use uhash::{meets_difficulty, UniversalHash};
#[derive(Serialize)]
struct JsonProve {
challenge: String,
difficulty: u32,
nonce: u64,
hash: String,
attempts: u64,
}
#[derive(Serialize)]
struct JsonVerify {
valid: bool,
hash_matches: bool,
difficulty_met: Option<bool>,
}
#[derive(Serialize)]
struct JsonInspect {
kind: &'static str,
bytes_len: usize,
hex: String,
}
pub(crate) fn cmd_prove(
challenge_hex: &str,
difficulty: u32,
start_nonce: u64,
max_attempts: u64,
json: bool,
) -> anyhow::Result<()> {
let challenge =
hex::decode(challenge_hex).map_err(|e| anyhow::anyhow!("Invalid challenge hex: {}", e))?;
if challenge.is_empty() {
anyhow::bail!("challenge must not be empty");
}
if max_attempts == 0 {
anyhow::bail!("max-attempts must be > 0");
}
let mut hasher = UniversalHash::new();
for attempt in 0..max_attempts {
let nonce = start_nonce.wrapping_add(attempt);
let mut input = Vec::with_capacity(challenge.len() + 8);
input.extend_from_slice(&challenge);
input.extend_from_slice(&nonce.to_le_bytes());
let hash = hasher.hash(&input);
if meets_difficulty(&hash, difficulty) {
if json {
let out = JsonProve {
challenge: challenge_hex.to_string(),
difficulty,
nonce,
hash: hex::encode(hash),
attempts: attempt + 1,
};
println!("{}", serde_json::to_string(&out)?);
} else {
println!("Found proof");
println!(" Nonce: {}", nonce);
println!(" Hash: {}", hex::encode(hash));
println!(" Attempts: {}", attempt + 1);
}
return Ok(());
}
}
anyhow::bail!(
"No proof found in {} attempts from nonce {}",
max_attempts,
start_nonce
);
}
pub(crate) fn cmd_verify(
challenge_hex: &str,
nonce: u64,
hash_hex: &str,
difficulty: Option<u32>,
json: bool,
) -> anyhow::Result<()> {
let challenge =
hex::decode(challenge_hex).map_err(|e| anyhow::anyhow!("Invalid challenge hex: {}", e))?;
let expected = hex::decode(hash_hex).map_err(|e| anyhow::anyhow!("Invalid hash hex: {}", e))?;
if expected.len() != 32 {
anyhow::bail!("hash must be exactly 32 bytes");
}
let mut hasher = UniversalHash::new();
let mut input = Vec::with_capacity(challenge.len() + 8);
input.extend_from_slice(&challenge);
input.extend_from_slice(&nonce.to_le_bytes());
let computed = hasher.hash(&input);
let hash_matches = expected == computed;
let difficulty_met = difficulty.map(|d| meets_difficulty(&computed, d));
let valid = hash_matches && difficulty_met.unwrap_or(true);
if json {
let out = JsonVerify {
valid,
hash_matches,
difficulty_met,
};
println!("{}", serde_json::to_string(&out)?);
} else {
println!("Verify result: {}", if valid { "valid" } else { "invalid" });
println!(" Hash matches: {}", hash_matches);
if let Some(ok) = difficulty_met {
println!(" Difficulty met: {}", ok);
}
println!(" Computed hash: {}", hex::encode(computed));
}
Ok(())
}
pub(crate) fn cmd_inspect(
challenge_hex: Option<&str>,
hash_hex: Option<&str>,
json: bool,
) -> anyhow::Result<()> {
let (kind, value) = match (challenge_hex, hash_hex) {
(Some(c), None) => ("challenge", c),
(None, Some(h)) => ("hash", h),
(Some(_), Some(_)) => anyhow::bail!("Use either --challenge or --hash, not both"),
(None, None) => anyhow::bail!("Provide --challenge or --hash"),
};
let bytes = hex::decode(value).map_err(|e| anyhow::anyhow!("Invalid {} hex: {}", kind, e))?;
if kind == "hash" && bytes.len() != 32 {
anyhow::bail!("hash must be exactly 32 bytes");
}
if json {
let out = JsonInspect {
kind,
bytes_len: bytes.len(),
hex: value.to_string(),
};
println!("{}", serde_json::to_string(&out)?);
} else {
println!("Inspect {}", kind);
println!(" Bytes: {}", bytes.len());
println!(" Hex: {}", value);
}
Ok(())
}