#![forbid(unsafe_code)]
pub mod context;
pub mod error;
mod prove;
pub mod wire;
pub const MS_CONTEXT_TAG: &[u8] = b"qssm-sdk-v1";
pub use context::{Proof, ProofContext};
pub use error::ZkError;
pub use prove::prove;
pub use wire::{ProofBundle, WireFormatError, PROTOCOL_VERSION};
#[cfg(test)]
mod tests {
use super::*;
use qssm_utils::hashing::blake3_hash;
use serde_json::json;
fn test_seed() -> [u8; 32] {
blake3_hash(b"QSSM-SDK-TEST-SEED")
}
fn test_entropy() -> [u8; 32] {
blake3_hash(b"QSSM-SDK-TEST-ENTROPY")
}
fn test_binding_ctx() -> [u8; 32] {
blake3_hash(b"test-binding-context")
}
fn test_template() -> qssm_templates::QssmTemplate {
qssm_templates::QssmTemplate::proof_of_age("test-age")
}
fn test_claim() -> serde_json::Value {
json!({ "claim": { "age_years": 25 } })
}
fn test_ctx() -> ProofContext {
ProofContext::new(test_seed())
}
fn make_proof() -> Proof {
prove(
&test_ctx(),
&test_template(),
&test_claim(),
100,
50,
test_binding_ctx(),
test_entropy(),
)
.expect("prove should succeed")
}
#[test]
fn prove_succeeds() {
let _proof = make_proof();
}
#[test]
fn prove_rejects_bad_predicate() {
let bad_claim = json!({ "claim": { "age_years": 15 } });
let err = prove(
&test_ctx(),
&test_template(),
&bad_claim,
100,
50,
test_binding_ctx(),
test_entropy(),
)
.unwrap_err();
assert!(matches!(err, ZkError::PredicateFailed(_)));
}
#[test]
fn prove_is_deterministic() {
let p1 = make_proof();
let p2 = make_proof();
let j1 = serde_json::to_string(&ProofBundle::from_proof(&p1)).unwrap();
let j2 = serde_json::to_string(&ProofBundle::from_proof(&p2)).unwrap();
assert_eq!(j1, j2, "identical inputs must produce identical proofs");
}
#[test]
fn wire_round_trip_json() {
let proof = make_proof();
let bundle = ProofBundle::from_proof(&proof);
let json = serde_json::to_string(&bundle).expect("serialize");
let bundle2: ProofBundle = serde_json::from_str(&json).expect("deserialize");
let recovered = bundle2.to_proof().expect("to_proof");
assert_eq!(recovered.ms_root(), proof.ms_root());
assert_eq!(recovered.value(), proof.value());
assert_eq!(recovered.target(), proof.target());
}
#[test]
fn wire_format_forward_compat() {
let proof = make_proof();
let bundle = ProofBundle::from_proof(&proof);
let json = serde_json::to_string(&bundle).expect("serialize");
let parsed: ProofBundle = serde_json::from_str(&json)
.expect("old bundle must remain parseable by current (and future) code");
let recovered = parsed.to_proof().expect("to_proof");
assert_eq!(recovered.ms_root(), proof.ms_root());
assert_eq!(recovered.value(), proof.value());
assert_eq!(recovered.target(), proof.target());
}
#[test]
fn wire_rejects_bad_version() {
let proof = make_proof();
let mut bundle = ProofBundle::from_proof(&proof);
bundle.version = 99;
let json = serde_json::to_string(&bundle).expect("serialize");
let parsed: ProofBundle = serde_json::from_str(&json).expect("deserialize");
let err = parsed.to_proof().unwrap_err();
assert!(matches!(err, WireFormatError::UnsupportedVersion(99)));
}
#[test]
fn wire_rejects_bad_hex() {
let proof = make_proof();
let mut bundle = ProofBundle::from_proof(&proof);
bundle.ms_root_hex = "ZZZZ_not_hex".to_string();
let json = serde_json::to_string(&bundle).expect("serialize");
let parsed: ProofBundle = serde_json::from_str(&json).expect("deserialize");
let err = parsed.to_proof().unwrap_err();
assert!(matches!(err, WireFormatError::HexDecode { .. }));
}
#[test]
fn wire_rejects_wrong_length() {
let proof = make_proof();
let mut bundle = ProofBundle::from_proof(&proof);
bundle.ms_root_hex = hex::encode([0u8; 16]);
let json = serde_json::to_string(&bundle).expect("serialize");
let parsed: ProofBundle = serde_json::from_str(&json).expect("deserialize");
let err = parsed.to_proof().unwrap_err();
assert!(matches!(
err,
WireFormatError::BadLength {
expected: 32,
got: 16,
..
}
));
}
#[test]
fn wire_rejects_wrong_coeff_count() {
let proof = make_proof();
let mut bundle = ProofBundle::from_proof(&proof);
bundle.le_commitment_coeffs = vec![0u32; 10];
let json = serde_json::to_string(&bundle).expect("serialize");
let parsed: ProofBundle = serde_json::from_str(&json).expect("deserialize");
let err = parsed.to_proof().unwrap_err();
assert!(matches!(
err,
WireFormatError::BadCoeffCount {
expected: 256,
got: 10,
..
}
));
}
#[test]
fn wire_rejects_unknown_fields() {
let proof = make_proof();
let bundle = ProofBundle::from_proof(&proof);
let mut json_val: serde_json::Value = serde_json::to_value(&bundle).expect("to_value");
json_val
.as_object_mut()
.unwrap()
.insert("smuggled_field".to_string(), serde_json::Value::Bool(true));
let json = serde_json::to_string(&json_val).expect("serialize");
let result = serde_json::from_str::<ProofBundle>(&json);
assert!(result.is_err(), "unknown fields must be rejected");
}
#[test]
fn proof_bundle_from_proof_injective() {
let proof_a = make_proof();
let proof_b = {
let different_entropy = blake3_hash(b"DIFFERENT-ENTROPY-SEED");
prove(
&test_ctx(),
&test_template(),
&test_claim(),
100,
50,
test_binding_ctx(),
different_entropy,
)
.expect("prove should succeed")
};
let json_a = serde_json::to_string(&ProofBundle::from_proof(&proof_a)).unwrap();
let json_b = serde_json::to_string(&ProofBundle::from_proof(&proof_b)).unwrap();
assert_ne!(
json_a, json_b,
"different proofs must produce different bundles"
);
}
#[test]
fn proof_bundle_preserves_all_fields() {
let proof = make_proof();
let bundle1 = ProofBundle::from_proof(&proof);
let recovered = bundle1.to_proof().expect("to_proof");
let bundle2 = ProofBundle::from_proof(&recovered);
let json1 = serde_json::to_string(&bundle1).unwrap();
let json2 = serde_json::to_string(&bundle2).unwrap();
assert_eq!(json1, json2, "round-trip must be lossless — no field drift");
}
#[test]
fn proof_bundle_json_field_names_stable() {
let proof = make_proof();
let bundle = ProofBundle::from_proof(&proof);
let val: serde_json::Value = serde_json::to_value(&bundle).expect("to_value");
let obj = val.as_object().expect("must be JSON object");
let mut keys: Vec<&str> = obj.keys().map(|k| k.as_str()).collect();
keys.sort();
let expected = vec![
"binding_entropy_hex",
"external_entropy_hex",
"external_entropy_included",
"le_challenge_seed_hex",
"le_commitment_coeffs",
"le_proof_t_coeffs",
"le_proof_z_coeffs",
"ms_bit_at_k",
"ms_challenge_hex",
"ms_k",
"ms_n",
"ms_opened_salt_hex",
"ms_path_hex",
"ms_root_hex",
"protocol_version",
"target",
"value",
"version",
];
assert_eq!(keys, expected, "JSON field names must match frozen schema");
}
}