Skip to content

Commit 6052bf7

Browse files
fix: proof verification 422 — reconstruct ZkProof envelope from stored data
ProofStore.submit() persists only request.proof_data (the raw proof bytes) but ProofVerifier.verify() was deserializing as a full ZkProof which requires proof_type, proof_data, timestamp, and metadata fields. This caused 422 "missing field proof_type" on GET /api/v1/proofs/{id}/verify. Fix: when direct ZkProof deserialization fails, reconstruct the envelope from StoredProof.proof_type metadata + the stored raw proof data. Also injects the ProofData serde type tag when the client omitted it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b59bc28 commit 6052bf7

1 file changed

Lines changed: 109 additions & 2 deletions

File tree

crates/aingle_cortex/src/proofs/verification.rs

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,61 @@ impl VerificationResult {
8989
}
9090
}
9191

92+
/// Reconstruct a `ZkProof` from a `StoredProof` whose `data` field contains
93+
/// only the raw `proof_data` JSON (without the ZkProof envelope).
94+
fn reconstruct_zk_proof(proof: &StoredProof) -> Result<aingle_zk::ZkProof, VerificationError> {
95+
let mut proof_data: serde_json::Value = serde_json::from_slice(&proof.data)
96+
.map_err(|e| VerificationError::DeserializationError(e.to_string()))?;
97+
98+
// Inject the ProofData serde tag ("type") if the client omitted it.
99+
// ProofData uses #[serde(tag = "type")] so deserialisation requires it.
100+
if let serde_json::Value::Object(ref mut map) = proof_data {
101+
if !map.contains_key("type") {
102+
let tag = cortex_to_proof_data_tag(&proof.proof_type);
103+
map.insert("type".into(), serde_json::Value::String(tag.into()));
104+
}
105+
}
106+
107+
let zk_type = cortex_to_zk_proof_type(&proof.proof_type);
108+
109+
let envelope = serde_json::json!({
110+
"proof_type": serde_json::to_value(&zk_type)
111+
.map_err(|e| VerificationError::DeserializationError(e.to_string()))?,
112+
"proof_data": proof_data,
113+
"timestamp": proof.created_at.timestamp() as u64,
114+
"metadata": null
115+
});
116+
117+
serde_json::from_value(envelope)
118+
.map_err(|e| VerificationError::DeserializationError(
119+
format!("Failed to reconstruct ZkProof envelope: {e}")
120+
))
121+
}
122+
123+
/// Map Cortex `ProofType` → `aingle_zk::ProofType`.
124+
fn cortex_to_zk_proof_type(pt: &ProofType) -> aingle_zk::ProofType {
125+
match pt {
126+
ProofType::Schnorr | ProofType::Knowledge | ProofType::HashOpening => {
127+
aingle_zk::ProofType::KnowledgeProof
128+
}
129+
ProofType::Equality => aingle_zk::ProofType::EqualityProof,
130+
ProofType::Membership => aingle_zk::ProofType::MembershipProof,
131+
ProofType::NonMembership => aingle_zk::ProofType::NonMembershipProof,
132+
ProofType::Range => aingle_zk::ProofType::RangeProof,
133+
}
134+
}
135+
136+
/// Map Cortex `ProofType` → `ProofData` serde tag value.
137+
fn cortex_to_proof_data_tag(pt: &ProofType) -> &'static str {
138+
match pt {
139+
ProofType::Schnorr | ProofType::Knowledge => "Knowledge",
140+
ProofType::HashOpening => "HashOpening",
141+
ProofType::Equality => "Equality",
142+
ProofType::Membership | ProofType::NonMembership => "Membership",
143+
ProofType::Range => "Knowledge",
144+
}
145+
}
146+
92147
/// Proof verifier that integrates with aingle_zk
93148
pub struct ProofVerifier {
94149
/// Configuration for verification
@@ -143,9 +198,13 @@ impl ProofVerifier {
143198
)));
144199
}
145200

146-
// Deserialize the proof data into aingle_zk::ZkProof
201+
// Deserialize the proof data into aingle_zk::ZkProof.
202+
// The stored data may be just the raw proof_data (without the ZkProof
203+
// envelope) when submitted via the REST API, since submit() only
204+
// persists request.proof_data. Try full envelope first, then
205+
// reconstruct from StoredProof.proof_type + raw proof data.
147206
let zk_proof: aingle_zk::ZkProof = serde_json::from_slice(&proof.data)
148-
.map_err(|e| VerificationError::DeserializationError(e.to_string()))?;
207+
.or_else(|_| reconstruct_zk_proof(proof))?;
149208

150209
// Verify based on proof type
151210
let valid = match proof.proof_type {
@@ -433,6 +492,54 @@ mod tests {
433492
assert!(verifier.config.strict_mode);
434493
}
435494

495+
/// Simulates the real REST API path: submit() stores only proof_data
496+
/// (without the ZkProof envelope), and verify() must reconstruct it.
497+
#[tokio::test]
498+
async fn test_verify_proof_stored_without_envelope() {
499+
let verifier = ProofVerifier::new();
500+
501+
// Create a valid hash-opening proof via aingle_zk
502+
let commitment = aingle_zk::HashCommitment::commit(b"test data");
503+
let zk_proof = aingle_zk::ZkProof::hash_opening(&commitment);
504+
505+
// Store ONLY the proof_data portion (what submit() actually does)
506+
let proof_data_only = serde_json::to_vec(&zk_proof.proof_data).unwrap();
507+
let stored = StoredProof::new(
508+
ProofType::HashOpening,
509+
proof_data_only,
510+
ProofMetadata::default(),
511+
);
512+
513+
// This is the exact path that was failing with 422
514+
let result = verifier.verify(&stored).await;
515+
assert!(result.is_ok(), "verify() should reconstruct ZkProof envelope: {:?}", result.err());
516+
assert!(result.unwrap().valid);
517+
}
518+
519+
/// Verify reconstruction works when client sends raw fields without serde tag.
520+
#[tokio::test]
521+
async fn test_verify_proof_without_type_tag() {
522+
let verifier = ProofVerifier::new();
523+
524+
// Client sends proof_data as raw JSON without the ProofData "type" tag
525+
let commitment = aingle_zk::HashCommitment::commit(b"test data");
526+
let raw = serde_json::json!({
527+
"commitment": commitment.hash,
528+
"salt": commitment.salt,
529+
});
530+
let raw_bytes = serde_json::to_vec(&raw).unwrap();
531+
532+
let stored = StoredProof::new(
533+
ProofType::HashOpening,
534+
raw_bytes,
535+
ProofMetadata::default(),
536+
);
537+
538+
let result = verifier.verify(&stored).await;
539+
assert!(result.is_ok(), "verify() should inject type tag: {:?}", result.err());
540+
assert!(result.unwrap().valid);
541+
}
542+
436543
#[tokio::test]
437544
async fn test_proof_size_limit() {
438545
let config = VerifierConfig {

0 commit comments

Comments
 (0)