Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -649,13 +669,37 @@ pub async fn revoke_document(Json(req): Json<RevokeRequest>) -> impl IntoRespons
)
}

pub async fn transfer_document(Json(req): Json<TransferRequest>) -> impl IntoResponse {
pub async fn transfer_document(
State(state): State<AppState>,
Json(req): Json<TransferRequest>,
) -> 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 (
Expand Down
1 change: 1 addition & 0 deletions contract/src/module/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod revocation_check;
52 changes: 52 additions & 0 deletions contract/src/module/revocation_check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use anyhow::Result;

use crate::cache::CacheBackend;

pub async fn is_revoked(cache: &CacheBackend, document_hash: &str) -> Result<bool> {
let key = format!("revoke:{}", document_hash);
is_revoked_with_lookup(|| async { cache.get_raw(&key).await }).await
}

async fn is_revoked_with_lookup<F, Fut>(lookup: F) -> Result<bool>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<Option<String>>>,
{
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");
}
}
Loading