uhash-cli 0.5.1

UniversalHash proof-of-work miner for Bostrom blockchain
Documentation
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(())
}