Skip to main content

aingle_cortex/rest/
proof_api.rs

1//! REST API endpoints for proof storage and verification
2
3use axum::{
4    extract::{Path, Query, State},
5    Json,
6};
7use serde::{Deserialize, Serialize};
8
9use crate::error::{Error, Result};
10use crate::middleware::{is_in_namespace, RequestNamespace};
11use crate::proofs::{ProofId, ProofMetadata, ProofType, StoredProof, SubmitProofRequest};
12use crate::state::AppState;
13
14/// Submit a new proof
15///
16/// POST /api/v1/proofs
17pub async fn submit_proof(
18    State(state): State<AppState>,
19    ns_ext: Option<axum::Extension<RequestNamespace>>,
20    Json(request): Json<SubmitProofRequest>,
21) -> Result<Json<SubmitProofResponse>> {
22    // Enforce namespace: submitter must belong to the namespace
23    if let Some(axum::Extension(RequestNamespace(Some(ref ns)))) = ns_ext {
24        if let Some(ref meta) = request.metadata {
25            if let Some(ref submitter) = meta.submitter {
26                if !is_in_namespace(submitter, ns) {
27                    return Err(Error::Forbidden(format!(
28                        "Submitter \"{}\" is not in namespace \"{}\"",
29                        submitter, ns
30                    )));
31                }
32            }
33        }
34    }
35
36    let proof_id = state
37        .proof_store
38        .submit(request)
39        .await
40        .map_err(|e| Error::ValidationError(e.to_string()))?;
41
42    let proof = state
43        .proof_store
44        .get(&proof_id)
45        .await
46        .ok_or_else(|| Error::Internal("Failed to retrieve submitted proof".to_string()))?;
47
48    Ok(Json(SubmitProofResponse {
49        proof_id,
50        submitted_at: proof.created_at,
51    }))
52}
53
54/// Submit multiple proofs in batch
55///
56/// POST /api/v1/proofs/batch
57pub async fn submit_proofs_batch(
58    State(state): State<AppState>,
59    Json(request): Json<BatchSubmitRequest>,
60) -> Result<Json<BatchSubmitResponse>> {
61    let results = state.proof_store.submit_batch(request.proofs).await;
62
63    let mut successful = Vec::new();
64    let mut failed = Vec::new();
65
66    for (idx, result) in results.into_iter().enumerate() {
67        match result {
68            Ok(proof_id) => successful.push(proof_id),
69            Err(e) => failed.push(BatchError {
70                index: idx,
71                error: e.to_string(),
72            }),
73        }
74    }
75
76    Ok(Json(BatchSubmitResponse {
77        successful_count: successful.len(),
78        failed_count: failed.len(),
79        successful,
80        failed,
81    }))
82}
83
84/// Get a proof by ID
85///
86/// GET /api/v1/proofs/:id
87pub async fn get_proof(
88    State(state): State<AppState>,
89    Path(proof_id): Path<ProofId>,
90) -> Result<Json<ProofResponse>> {
91    let proof = state
92        .proof_store
93        .get(&proof_id)
94        .await
95        .ok_or_else(|| Error::NotFound(format!("Proof {} not found", proof_id)))?;
96
97    Ok(Json(ProofResponse::from(proof)))
98}
99
100/// Verify a proof
101///
102/// GET /api/v1/proofs/:id/verify
103pub async fn verify_proof_by_id(
104    State(state): State<AppState>,
105    Path(proof_id): Path<ProofId>,
106) -> Result<Json<VerifyProofResponse>> {
107    let result = state
108        .proof_store
109        .verify(&proof_id)
110        .await
111        .map_err(|e| Error::ValidationError(e.to_string()))?;
112
113    Ok(Json(VerifyProofResponse {
114        proof_id: proof_id.clone(),
115        valid: result.valid,
116        verified_at: result.verified_at,
117        details: result.details,
118        verification_time_us: result.verification_time_us,
119    }))
120}
121
122/// Batch verify multiple proofs
123///
124/// POST /api/v1/proofs/verify/batch
125pub async fn verify_proofs_batch(
126    State(state): State<AppState>,
127    Json(request): Json<BatchVerifyRequest>,
128) -> Result<Json<BatchVerifyResponse>> {
129    let results = state.proof_store.batch_verify(&request.proof_ids).await;
130
131    let mut verifications = Vec::new();
132    for (proof_id, result) in request.proof_ids.iter().zip(results.into_iter()) {
133        match result {
134            Ok(verification) => {
135                verifications.push(VerifyProofResponse {
136                    proof_id: proof_id.clone(),
137                    valid: verification.valid,
138                    verified_at: verification.verified_at,
139                    details: verification.details,
140                    verification_time_us: verification.verification_time_us,
141                });
142            }
143            Err(e) => {
144                verifications.push(VerifyProofResponse {
145                    proof_id: proof_id.clone(),
146                    valid: false,
147                    verified_at: chrono::Utc::now(),
148                    details: vec![format!("Verification error: {}", e)],
149                    verification_time_us: 0,
150                });
151            }
152        }
153    }
154
155    let valid_count = verifications.iter().filter(|v| v.valid).count();
156
157    Ok(Json(BatchVerifyResponse {
158        total: verifications.len(),
159        valid_count,
160        invalid_count: verifications.len() - valid_count,
161        verifications,
162    }))
163}
164
165/// List proofs with optional filters
166///
167/// GET /api/v1/proofs
168pub async fn list_proofs(
169    State(state): State<AppState>,
170    ns_ext: Option<axum::Extension<RequestNamespace>>,
171    Query(params): Query<ListProofsQuery>,
172) -> Result<Json<ListProofsResponse>> {
173    let proofs = state.proof_store.list(params.proof_type).await;
174
175    let mut filtered_proofs = proofs;
176
177    // Filter by namespace: only show proofs whose submitter is in the namespace
178    if let Some(axum::Extension(RequestNamespace(Some(ref ns)))) = ns_ext {
179        filtered_proofs.retain(|p| {
180            p.metadata
181                .submitter
182                .as_deref()
183                .map(|s| is_in_namespace(s, ns))
184                .unwrap_or(false)
185        });
186    }
187
188    // Apply verified filter
189    if let Some(verified) = params.verified {
190        filtered_proofs.retain(|p| p.verified == verified);
191    }
192
193    // Apply limit
194    let limit = params.limit.unwrap_or(100).min(1000);
195    filtered_proofs.truncate(limit);
196
197    let proofs_response: Vec<ProofResponse> = filtered_proofs
198        .into_iter()
199        .map(ProofResponse::from)
200        .collect();
201
202    Ok(Json(ListProofsResponse {
203        count: proofs_response.len(),
204        proofs: proofs_response,
205    }))
206}
207
208/// Delete a proof
209///
210/// DELETE /api/v1/proofs/:id
211pub async fn delete_proof(
212    State(state): State<AppState>,
213    ns_ext: Option<axum::Extension<RequestNamespace>>,
214    Path(proof_id): Path<ProofId>,
215) -> Result<Json<DeleteProofResponse>> {
216    // Enforce namespace: verify the proof's submitter is in the namespace
217    if let Some(axum::Extension(RequestNamespace(Some(ref ns)))) = ns_ext {
218        if let Some(proof) = state.proof_store.get(&proof_id).await {
219            if let Some(ref submitter) = proof.metadata.submitter {
220                if !is_in_namespace(submitter, ns) {
221                    return Err(Error::Forbidden(format!(
222                        "Proof submitter is not in namespace \"{}\"",
223                        ns
224                    )));
225                }
226            }
227        }
228    }
229
230    let deleted = state.proof_store.delete(&proof_id).await;
231
232    if deleted {
233        Ok(Json(DeleteProofResponse {
234            proof_id,
235            deleted: true,
236        }))
237    } else {
238        Err(Error::NotFound(format!("Proof {} not found", proof_id)))
239    }
240}
241
242/// Get proof statistics
243///
244/// GET /api/v1/proofs/stats
245pub async fn get_proof_stats(State(state): State<AppState>) -> Result<Json<ProofStatsResponse>> {
246    let stats = state.proof_store.stats().await;
247
248    Ok(Json(ProofStatsResponse {
249        total_proofs: stats.total_proofs,
250        proofs_by_type: stats.proofs_by_type,
251        total_verifications: stats.total_verifications,
252        successful_verifications: stats.successful_verifications,
253        failed_verifications: stats.failed_verifications,
254        cache_hits: stats.cache_hits,
255        cache_misses: stats.cache_misses,
256        cache_hit_rate: if stats.cache_hits + stats.cache_misses > 0 {
257            stats.cache_hits as f64 / (stats.cache_hits + stats.cache_misses) as f64
258        } else {
259            0.0
260        },
261        total_size_bytes: stats.total_size_bytes,
262    }))
263}
264
265// Request/Response DTOs
266
267#[derive(Debug, Serialize)]
268pub struct SubmitProofResponse {
269    pub proof_id: ProofId,
270    pub submitted_at: chrono::DateTime<chrono::Utc>,
271}
272
273#[derive(Debug, Deserialize)]
274pub struct BatchSubmitRequest {
275    pub proofs: Vec<SubmitProofRequest>,
276}
277
278#[derive(Debug, Serialize)]
279pub struct BatchSubmitResponse {
280    pub successful_count: usize,
281    pub failed_count: usize,
282    pub successful: Vec<ProofId>,
283    pub failed: Vec<BatchError>,
284}
285
286#[derive(Debug, Serialize)]
287pub struct BatchError {
288    pub index: usize,
289    pub error: String,
290}
291
292#[derive(Debug, Serialize)]
293pub struct ProofResponse {
294    pub id: ProofId,
295    pub proof_type: ProofType,
296    pub created_at: chrono::DateTime<chrono::Utc>,
297    pub verified: bool,
298    pub verified_at: Option<chrono::DateTime<chrono::Utc>>,
299    pub metadata: ProofMetadata,
300    pub size_bytes: usize,
301}
302
303impl From<StoredProof> for ProofResponse {
304    fn from(proof: StoredProof) -> Self {
305        let size_bytes = proof.size_bytes();
306        Self {
307            id: proof.id,
308            proof_type: proof.proof_type,
309            created_at: proof.created_at,
310            verified: proof.verified,
311            verified_at: proof.verified_at,
312            metadata: proof.metadata,
313            size_bytes,
314        }
315    }
316}
317
318#[derive(Debug, Serialize)]
319pub struct VerifyProofResponse {
320    pub proof_id: ProofId,
321    pub valid: bool,
322    pub verified_at: chrono::DateTime<chrono::Utc>,
323    pub details: Vec<String>,
324    pub verification_time_us: u64,
325}
326
327#[derive(Debug, Deserialize)]
328pub struct BatchVerifyRequest {
329    pub proof_ids: Vec<ProofId>,
330}
331
332#[derive(Debug, Serialize)]
333pub struct BatchVerifyResponse {
334    pub total: usize,
335    pub valid_count: usize,
336    pub invalid_count: usize,
337    pub verifications: Vec<VerifyProofResponse>,
338}
339
340#[derive(Debug, Deserialize)]
341pub struct ListProofsQuery {
342    pub proof_type: Option<ProofType>,
343    pub verified: Option<bool>,
344    pub limit: Option<usize>,
345}
346
347#[derive(Debug, Serialize)]
348pub struct ListProofsResponse {
349    pub count: usize,
350    pub proofs: Vec<ProofResponse>,
351}
352
353#[derive(Debug, Serialize)]
354pub struct DeleteProofResponse {
355    pub proof_id: ProofId,
356    pub deleted: bool,
357}
358
359#[derive(Debug, Serialize)]
360pub struct ProofStatsResponse {
361    pub total_proofs: usize,
362    pub proofs_by_type: std::collections::HashMap<String, usize>,
363    pub total_verifications: usize,
364    pub successful_verifications: usize,
365    pub failed_verifications: usize,
366    pub cache_hits: usize,
367    pub cache_misses: usize,
368    pub cache_hit_rate: f64,
369    pub total_size_bytes: usize,
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use crate::state::AppState;
376    use axum::extract::State as AxumState;
377
378    #[tokio::test]
379    async fn test_submit_and_get_proof() {
380        let state = AppState::new();
381
382        let request = SubmitProofRequest {
383            proof_type: ProofType::Knowledge,
384            proof_data: serde_json::json!({
385                "commitment": vec![0u8; 32],
386                "challenge": vec![1u8; 32],
387                "response": vec![2u8; 32],
388            }),
389            metadata: None,
390        };
391
392        let response = submit_proof(AxumState(state.clone()), None, Json(request))
393            .await
394            .unwrap();
395
396        let proof_id = response.0.proof_id.clone();
397        assert!(!proof_id.is_empty());
398
399        let get_response = get_proof(AxumState(state), Path(proof_id)).await.unwrap();
400
401        assert_eq!(get_response.0.proof_type, ProofType::Knowledge);
402    }
403
404    #[tokio::test]
405    async fn test_list_proofs() {
406        let state = AppState::new();
407
408        // Submit multiple proofs
409        for _ in 0..3 {
410            let request = SubmitProofRequest {
411                proof_type: ProofType::Schnorr,
412                proof_data: serde_json::json!({"test": "data"}),
413                metadata: None,
414            };
415            submit_proof(AxumState(state.clone()), None, Json(request))
416                .await
417                .unwrap();
418        }
419
420        let query = ListProofsQuery {
421            proof_type: None,
422            verified: None,
423            limit: Some(10),
424        };
425
426        let response = list_proofs(AxumState(state), None, Query(query)).await.unwrap();
427
428        assert_eq!(response.0.count, 3);
429    }
430
431    #[tokio::test]
432    async fn test_proof_stats() {
433        let state = AppState::new();
434
435        let request = SubmitProofRequest {
436            proof_type: ProofType::Equality,
437            proof_data: serde_json::json!({"test": "data"}),
438            metadata: None,
439        };
440
441        submit_proof(AxumState(state.clone()), None, Json(request))
442            .await
443            .unwrap();
444
445        let response = get_proof_stats(AxumState(state)).await.unwrap();
446
447        assert_eq!(response.0.total_proofs, 1);
448    }
449}