bloclawd 0.1.2

Live cohort percentiles for Claude Code and Codex rate limits — see where Pro, Max5, and Max20 caps actually fire and how they drift week to week. Anonymous CLI submission, open dataset, k-anonymized at n ≥ 5.
Documentation
//! PoW solver wrapper.
//!
//! Thin glue around bloclawd_pow::solve. The 72-byte input layout stays inside
//! crates/pow (package: bloclawd-pow); this layer only canonicalizes the payload and supplies K=22.

use std::time::{Duration, Instant};

use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use bloclawd_pow::{ChallengeId, K_V1, Nonce, PayloadHash, PowError};
use bloclawd_schema::EventPayload;

use crate::canonical::{canonicalize, payload_hash};
use crate::wire_error::IngestCliError;

pub fn decode_challenge_id(b64: &str) -> Result<ChallengeId, IngestCliError> {
    let bytes = URL_SAFE_NO_PAD
        .decode(b64)
        .map_err(|_| IngestCliError::ServerUnavailable)?;
    let arr: [u8; 32] = bytes
        .as_slice()
        .try_into()
        .map_err(|_| IngestCliError::ServerUnavailable)?;
    Ok(ChallengeId(arr))
}

pub fn solve_for_payload(
    payload: &EventPayload,
    challenge_id: &ChallengeId,
) -> Result<(Nonce, PayloadHash), IngestCliError> {
    let deadline = Instant::now() + Duration::from_secs(30);
    solve_until_deadline(payload, challenge_id, deadline)
}

pub fn solve_until_deadline(
    payload: &EventPayload,
    challenge_id: &ChallengeId,
    deadline: Instant,
) -> Result<(Nonce, PayloadHash), IngestCliError> {
    let canonical = canonicalize(payload).map_err(|_| IngestCliError::SchemaMismatch)?;
    let ph = PayloadHash(payload_hash(&canonical));
    match bloclawd_pow::solve(challenge_id, &ph, K_V1, 0, deadline) {
        Ok((nonce, _hash)) => Ok((nonce, ph)),
        Err(PowError::Timeout) => Err(IngestCliError::PowTimeout),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use base64::Engine;
    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
    use bloclawd_pow::ChallengeId;
    use bloclawd_schema::{Harness, Model, Region, Tier, TokenCounts};
    use std::time::Instant;

    fn sample_payload() -> EventPayload {
        EventPayload {
            v: 1,
            model: Model::ClaudeSonnet45,
            tier: Tier::Max20,
            harness: Harness::ClaudeCode,
            region: Region::Na,
            tokens: TokenCounts {
                input_tokens: 5,
                output_tokens: 6,
                cache_read_input_tokens: 7,
                ephemeral_5m_input_tokens: 8,
                ephemeral_1h_input_tokens: 9,
                cached_input_tokens: 0,
                reasoning_output_tokens: 0,
            },
        }
    }

    #[test]
    fn solve_for_payload_finds_nonce_at_k22() {
        let payload = sample_payload();
        let mut cid_bytes = [0_u8; 32];
        cid_bytes[29..32].copy_from_slice(&[0x03, 0x82, 0x12]);
        let cid = ChallengeId(cid_bytes);
        let (nonce, payload_hash) =
            solve_for_payload(&payload, &cid).expect("PoW solves within timeout");
        let hash = bloclawd_pow::pow_hash(&cid, &payload_hash, &nonce);
        assert!(bloclawd_pow::leading_zero_bits(&hash) >= bloclawd_pow::K_V1);
    }

    #[test]
    fn expired_deadline_returns_pow_timeout() {
        let payload = sample_payload();
        let cid = ChallengeId([0_u8; 32]);
        let err = solve_until_deadline(&payload, &cid, Instant::now())
            .expect_err("expired deadline rejects");
        assert_eq!(err, crate::IngestCliError::PowTimeout);
    }

    #[test]
    fn decode_challenge_id_accepts_base64url_32_bytes() {
        let encoded = URL_SAFE_NO_PAD.encode([9_u8; 32]);
        let cid = decode_challenge_id(&encoded).expect("valid challenge id");
        assert_eq!(cid, ChallengeId([9_u8; 32]));
    }

    #[test]
    fn decode_challenge_id_rejects_malformed_base64() {
        let err = decode_challenge_id("not@@base64").expect_err("invalid base64 rejects");
        assert_eq!(err, crate::IngestCliError::ServerUnavailable);
    }
}