@@ -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
93148pub 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