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);
}
}