diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 864d3d8..3c4de07 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -4,6 +4,7 @@ pub mod event; pub mod expiry; pub mod hash_validator; pub mod metrics; +pub mod module; pub mod multi_party; pub mod rate_limit; pub mod stellar; @@ -27,6 +28,7 @@ use tracing::{info, warn}; use cache::CacheBackend; use hash_validator::{HashValidator, ValidationError as HashValidationError}; use metrics::MetricsRegistry; +use module::revocation_check::is_revoked; use stellar::{StellarClient, TransactionRecord}; // Application state @@ -597,6 +599,24 @@ pub async fn submit_document( } } + match is_revoked(&state.cache, &normalized_hash).await { + Ok(true) => { + return ( + StatusCode::CONFLICT, + Json(ValidationErrorResponse { + error: "document has been revoked".to_string(), + }), + ) + .into_response(); + } + Ok(false) => {} + Err(e) => { + warn!("Cache revocation check failed during submit: {}", e); + state.metrics.increment_error_count(); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + } + let tx_hash = match state.stellar.anchor_hash(&normalized_hash).await { Ok(tx) => tx, Err(e) => { @@ -649,13 +669,37 @@ pub async fn revoke_document(Json(req): Json) -> impl IntoRespons ) } -pub async fn transfer_document(Json(req): Json) -> impl IntoResponse { +pub async fn transfer_document( + State(state): State, + Json(req): Json, +) -> impl IntoResponse { let normalized_hash = HashValidator::normalize(&req.document_hash); if let Err(err) = HashValidator::validate_sha256(&normalized_hash) { let (status, body) = map_validation_error(err); return (status, Json(body)); } + match is_revoked(&state.cache, &normalized_hash).await { + Ok(true) => { + return ( + StatusCode::CONFLICT, + Json(ValidationErrorResponse { + error: "document has been revoked".to_string(), + }), + ); + } + Ok(false) => {} + Err(e) => { + warn!("Cache revocation check failed during transfer: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ValidationErrorResponse { + error: "failed to check revocation status".to_string(), + }), + ); + } + } + // Basic date validation: expect YYYY-MM-DD if chrono::NaiveDate::parse_from_str(&req.transfer_date, "%Y-%m-%d").is_err() { return ( diff --git a/contract/src/module/mod.rs b/contract/src/module/mod.rs new file mode 100644 index 0000000..7648dca --- /dev/null +++ b/contract/src/module/mod.rs @@ -0,0 +1 @@ +pub mod revocation_check; diff --git a/contract/src/module/revocation_check.rs b/contract/src/module/revocation_check.rs new file mode 100644 index 0000000..e4eae10 --- /dev/null +++ b/contract/src/module/revocation_check.rs @@ -0,0 +1,52 @@ +use anyhow::Result; + +use crate::cache::CacheBackend; + +pub async fn is_revoked(cache: &CacheBackend, document_hash: &str) -> Result { + let key = format!("revoke:{}", document_hash); + is_revoked_with_lookup(|| async { cache.get_raw(&key).await }).await +} + +async fn is_revoked_with_lookup(lookup: F) -> Result +where + F: FnOnce() -> Fut, + Fut: std::future::Future>>, +{ + Ok(lookup().await?.is_some()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cache::{CacheBackend, InMemoryCache}; + use anyhow::anyhow; + + fn sample_hash() -> &'static str { + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + + #[tokio::test] + async fn returns_false_when_revocation_key_is_absent() { + let cache = CacheBackend::InMemory(InMemoryCache::new()); + let revoked = is_revoked(&cache, sample_hash()).await.unwrap(); + assert!(!revoked); + } + + #[tokio::test] + async fn returns_true_when_revocation_key_exists() { + let cache = CacheBackend::InMemory(InMemoryCache::new()); + let key = format!("revoke:{}", sample_hash()); + cache.set_raw(&key, "revoked", 60).await.unwrap(); + + let revoked = is_revoked(&cache, sample_hash()).await.unwrap(); + assert!(revoked); + } + + #[tokio::test] + async fn returns_error_when_lookup_fails() { + let error = is_revoked_with_lookup(|| async { Err(anyhow!("redis failure")) }) + .await + .unwrap_err(); + assert_eq!(error.to_string(), "redis failure"); + } +}