#![forbid(unsafe_code)]
mod commit_impl;
use qssm_local_prover::ProofContext;
use qssm_utils::hashing::blake3_hash;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct WireBlueprint {
seed_hex: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
template_id: Option<String>,
template_json: String,
}
#[derive(Serialize, Deserialize)]
struct WireZkProof {
bundle: qssm_local_prover::ProofBundle,
claim: serde_json::Value,
binding_ctx_hex: String,
}
pub fn compile(template_id: &str) -> Result<Vec<u8>, String> {
let template = resolve_template_input(template_id)?;
let seed = qssm_entropy::harvest(&qssm_entropy::HarvestConfig::default())
.map_err(|e| format!("entropy unavailable: {e}"))?
.to_seed();
let wire = WireBlueprint {
seed_hex: hex::encode(seed),
template_id: Some(template.id().to_string()),
template_json: serde_json::to_string(&template)
.map_err(|e| format!("template serialization failed: {e}"))?,
};
serde_json::to_vec(&wire).map_err(|e| format!("serialization failed: {e}"))
}
#[must_use]
pub fn commit(secret: &[u8], salt: &[u8; 32]) -> Vec<u8> {
commit_impl::commit_hash(secret, salt).to_vec()
}
pub fn prove(secret: &[u8], salt: &[u8; 32], blueprint: &[u8]) -> Result<Vec<u8>, String> {
let wire_bp: WireBlueprint =
serde_json::from_slice(blueprint).map_err(|e| format!("invalid blueprint: {e}"))?;
let seed = decode_hex_32(&wire_bp.seed_hex, "blueprint seed")?;
let template = template_from_blueprint(&wire_bp)?;
let ctx = ProofContext::new(seed);
let claim: serde_json::Value =
serde_json::from_slice(secret).map_err(|e| format!("invalid JSON claim: {e}"))?;
let binding_ctx = blake3_hash(salt);
let entropy_seed = qssm_entropy::harvest(&qssm_entropy::HarvestConfig::default())
.map_err(|e| format!("entropy unavailable: {e}"))?
.to_seed();
let (value, target) = extract_value_target(&claim, &template);
let proof = qssm_local_prover::prove(
&ctx,
&template,
&claim,
value,
target,
binding_ctx,
entropy_seed,
)
.map_err(|e| format!("prove failed: {e}"))?;
let wire = WireZkProof {
bundle: qssm_local_prover::ProofBundle::from_proof(&proof),
claim,
binding_ctx_hex: hex::encode(binding_ctx),
};
serde_json::to_vec(&wire).map_err(|e| format!("serialization failed: {e}"))
}
#[must_use]
pub fn verify(proof: &[u8], blueprint: &[u8]) -> bool {
verify_inner(proof, blueprint).unwrap_or(false)
}
#[must_use]
pub fn open(secret: &[u8], salt: &[u8; 32]) -> Vec<u8> {
commit_impl::commit_hash(secret, salt).to_vec()
}
fn verify_inner(proof: &[u8], blueprint: &[u8]) -> Result<bool, String> {
let wire_bp: WireBlueprint =
serde_json::from_slice(blueprint).map_err(|e| format!("invalid blueprint: {e}"))?;
let seed = decode_hex_32(&wire_bp.seed_hex, "blueprint seed")?;
let template = template_from_blueprint(&wire_bp)?;
let ctx = ProofContext::new(seed);
let wire_proof: WireZkProof =
serde_json::from_slice(proof).map_err(|e| format!("invalid proof: {e}"))?;
let binding_ctx = decode_hex_32(&wire_proof.binding_ctx_hex, "binding_ctx")?;
let inner_proof = wire_proof
.bundle
.to_proof()
.map_err(|e| format!("invalid proof bundle: {e}"))?;
qssm_local_verifier::verify(
&ctx,
&template,
&wire_proof.claim,
&inner_proof,
binding_ctx,
)
.map_err(|e| format!("verification failed: {e}"))
}
fn decode_hex_32(hex_str: &str, field: &str) -> Result<[u8; 32], String> {
let bytes = hex::decode(hex_str).map_err(|e| format!("invalid hex for {field}: {e}"))?;
<[u8; 32]>::try_from(bytes.as_slice())
.map_err(|_| format!("{field}: expected 32 bytes, got {}", bytes.len()))
}
fn resolve_template_input(raw: &str) -> Result<qssm_templates::QssmTemplate, String> {
if let Some(template) = qssm_templates::resolve(raw.trim()) {
return Ok(template);
}
qssm_templates::QssmTemplate::from_json_slice(raw.as_bytes())
.map_err(|_| format!("unknown template or invalid template JSON: {raw}"))
}
fn template_from_blueprint(
wire_bp: &WireBlueprint,
) -> Result<qssm_templates::QssmTemplate, String> {
if !wire_bp.template_json.trim().is_empty() {
return qssm_templates::QssmTemplate::from_json_slice(wire_bp.template_json.as_bytes())
.map_err(|e| format!("invalid blueprint template: {e}"));
}
if let Some(template_id) = &wire_bp.template_id {
return qssm_templates::resolve(template_id)
.ok_or_else(|| format!("unknown template: {template_id}"));
}
Err("blueprint is missing template payload".to_string())
}
fn extract_value_target(
claim: &serde_json::Value,
template: &qssm_templates::QssmTemplate,
) -> (u64, u64) {
use qssm_templates::{json_at_path, PredicateBlock};
for pred in template.predicates() {
match pred {
PredicateBlock::Range { field, min, .. } => {
if let Some(val) = json_at_path(claim, field).and_then(|v| v.as_u64()) {
return (val, (*min as u64).saturating_sub(1));
}
}
PredicateBlock::AtLeast { field, min } => {
if let Some(val) = json_at_path(claim, field).and_then(|v| v.as_u64()) {
return (val, (*min as u64).saturating_sub(1));
}
}
PredicateBlock::Compare {
field,
op: qssm_templates::CmpOp::Gt,
rhs,
} => {
if let (Some(lhs), Some(rhs_val)) = (
json_at_path(claim, field).and_then(|v| v.as_u64()),
rhs.as_u64(),
) {
return (lhs, rhs_val);
}
}
_ => {}
}
}
(1, 0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn commit_open_round_trip() {
let secret = b"my-secret-value";
let salt = [42u8; 32];
let c = commit(secret, &salt);
let d = open(secret, &salt);
assert_eq!(c, d);
}
#[test]
fn open_rejects_wrong_secret() {
let salt = [42u8; 32];
let c = commit(b"correct", &salt);
let d = open(b"wrong", &salt);
assert_ne!(c, d);
}
#[test]
fn extract_value_target_age_gate() {
let template = qssm_templates::QssmTemplate::proof_of_age("age-gate-21");
let claim = serde_json::json!({ "claim": { "age_years": 25 } });
let (v, t) = extract_value_target(&claim, &template);
assert_eq!(v, 25);
assert_eq!(t, 20);
}
#[test]
fn compile_rejects_unknown_template() {
let result = compile("nonexistent-template-xyz");
assert!(result.is_err());
assert!(result.unwrap_err().contains("unknown template"));
}
#[test]
fn prove_value_equals_min_passes() {
let blueprint = compile("age-gate-21").unwrap();
let claim = br#"{"claim":{"age_years":21}}"#;
let salt = [1u8; 32];
let proof = prove(claim, &salt, &blueprint);
assert!(
proof.is_ok(),
"age=21 should pass age-gate-21: {}",
proof.unwrap_err()
);
assert!(verify(&proof.unwrap(), &blueprint));
}
#[test]
fn prove_value_above_min_passes() {
let blueprint = compile("age-gate-21").unwrap();
let claim = br#"{"claim":{"age_years":30}}"#;
let salt = [2u8; 32];
let proof = prove(claim, &salt, &blueprint);
assert!(proof.is_ok(), "age=30 should pass: {}", proof.unwrap_err());
assert!(verify(&proof.unwrap(), &blueprint));
}
#[test]
fn prove_value_below_min_fails() {
let blueprint = compile("age-gate-21").unwrap();
let claim = br#"{"claim":{"age_years":20}}"#;
let salt = [3u8; 32];
let proof = prove(claim, &salt, &blueprint);
assert!(proof.is_err(), "age=20 should fail age-gate-21");
}
#[test]
fn compile_accepts_raw_template_json() {
let template = serde_json::json!({
"qssm_template_version": 1,
"id": "custom-age-gate",
"title": "Custom age gate",
"allowed_anchor_kinds": ["anchor_hash", "static_root", "timestamp_unix_secs"],
"predicates": [
{
"kind": "at_least",
"field": "claim.age_years",
"min": 21
}
]
});
let blueprint =
compile(&template.to_string()).expect("custom template JSON should compile");
let proof = prove(br#"{"claim":{"age_years":30}}"#, &[7u8; 32], &blueprint)
.expect("custom template blueprint should prove");
assert!(verify(&proof, &blueprint));
}
}