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
1 change: 1 addition & 0 deletions backend/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,5 @@ def invalidate(self):
resolution_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1)
visit_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=2)
audit_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=2)
closure_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1)
user_issues_cache = ThreadSafeCache(ttl=300, max_size=50) # 5 minutes TTL
31 changes: 29 additions & 2 deletions backend/closure_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from datetime import datetime, timedelta, timezone
from backend.models import Grievance, GrievanceFollower, ClosureConfirmation, GrievanceStatus
import logging
import hashlib
import hmac
from backend.cache import closure_last_hash_cache
from backend.config import get_auth_config

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -86,16 +90,39 @@ def submit_confirmation(grievance_id: int, user_email: str, confirmation_type: s
if existing:
raise ValueError("You have already submitted a response for this closure")

# Blockchain feature: calculate integrity hash for the closure confirmation
# Performance Boost: Use thread-safe cache to eliminate DB query for last hash
prev_hash = closure_last_hash_cache.get("last_hash")
Comment thread
RohanExploit marked this conversation as resolved.
if prev_hash is None:
# Cache miss: Fetch only the last hash from DB
last_record = db.query(ClosureConfirmation.integrity_hash).order_by(ClosureConfirmation.id.desc()).first()
prev_hash = last_record[0] if last_record and last_record[0] else ""
closure_last_hash_cache.set(data=prev_hash, key="last_hash")
Comment thread
RohanExploit marked this conversation as resolved.

Comment thread
RohanExploit marked this conversation as resolved.
# Chaining logic: hash(grievance_id|user_email|confirmation_type|prev_hash)
hash_content = f"{grievance_id}|{user_email}|{confirmation_type}|{prev_hash}"
secret_key = get_auth_config().secret_key
integrity_hash = hmac.new(
secret_key.encode('utf-8'),
hash_content.encode('utf-8'),
hashlib.sha256
).hexdigest()

# Create confirmation record
confirmation = ClosureConfirmation(
grievance_id=grievance_id,
user_email=user_email,
confirmation_type=confirmation_type,
reason=reason
reason=reason,
integrity_hash=integrity_hash,
previous_integrity_hash=prev_hash
Comment thread
RohanExploit marked this conversation as resolved.
)
db.add(confirmation)
db.commit()


# Update cache after successful commit
closure_last_hash_cache.set(data=integrity_hash, key="last_hash")

# Check if threshold is met
return ClosureService.check_and_finalize_closure(grievance_id, db)

Expand Down
28 changes: 6 additions & 22 deletions backend/grievance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,29 +87,13 @@ def create_grievance(self, grievance_data: Dict[str, Any], db: Session = None) -
unique_id = str(uuid.uuid4())[:8].upper()

# Blockchain integrity logic
# We cache both the last grievance ID and its integrity hash, and validate
# the cache against the current DB state to avoid chaining to stale hashes
cached_prev_hash = grievance_last_hash_cache.get("last_hash")
cached_last_id = grievance_last_hash_cache.get("last_id")

# Always check the actual last grievance in the DB
last_grievance = db.query(Grievance.id, Grievance.integrity_hash).order_by(Grievance.id.desc()).first()
if last_grievance:
db_last_id, db_last_hash = last_grievance
else:
db_last_id, db_last_hash = None, ""

# If cache is missing or inconsistent with DB, refresh from DB
if (
cached_prev_hash is None
or cached_last_id != db_last_id
or cached_prev_hash != db_last_hash
):
prev_hash = db_last_hash or ""
# Performance Boost: Use thread-safe cache to eliminate DB query for last hash
prev_hash = grievance_last_hash_cache.get("last_hash")
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Removing the DB-validation step from the blockchain cache creates a silent chain-corruption risk. The old code always compared the cached last_hash/last_id against the actual DB state to detect stale entries. The new code trusts the in-memory cache for up to 1 hour (the TTL in cache.py). If any concurrent request, worker, or external process inserts a grievance during that window, subsequent grievances will chain to a stale prev_hash, permanently forking the integrity chain.

Consider restoring the DB cross-check, or at minimum verifying last_id hasn't changed before trusting the cached hash.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/grievance_service.py, line 91:

<comment>Removing the DB-validation step from the blockchain cache creates a silent chain-corruption risk. The old code always compared the cached `last_hash`/`last_id` against the actual DB state to detect stale entries. The new code trusts the in-memory cache for up to 1 hour (the TTL in `cache.py`). If any concurrent request, worker, or external process inserts a grievance during that window, subsequent grievances will chain to a stale `prev_hash`, permanently forking the integrity chain.

Consider restoring the DB cross-check, or at minimum verifying `last_id` hasn't changed before trusting the cached hash.</comment>

<file context>
@@ -87,29 +87,13 @@ def create_grievance(self, grievance_data: Dict[str, Any], db: Session = None) -
-            ):
-                prev_hash = db_last_hash or ""
+            # Performance Boost: Use thread-safe cache to eliminate DB query for last hash
+            prev_hash = grievance_last_hash_cache.get("last_hash")
+            if prev_hash is None:
+                # Cache miss: Fetch only the last hash from DB
</file context>
Fix with Cubic

if prev_hash is None:
# Cache miss: Fetch only the last hash from DB
last_grievance = db.query(Grievance.integrity_hash).order_by(Grievance.id.desc()).first()
prev_hash = last_grievance[0] if last_grievance and last_grievance[0] else ""
grievance_last_hash_cache.set(data=prev_hash, key="last_hash")
Comment thread
RohanExploit marked this conversation as resolved.
Comment thread
RohanExploit marked this conversation as resolved.
grievance_last_hash_cache.set(data=db_last_id, key="last_id")
else:
prev_hash = cached_prev_hash or ""

# Chaining: hash(unique_id|category|severity|prev_hash)
hash_content = f"{unique_id}|{grievance_data.get('category', 'general')}|{severity.value}|{prev_hash}"
Expand Down
13 changes: 13 additions & 0 deletions backend/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,19 @@ def index_exists(table, index_name):
if not index_exists("escalation_audits", "ix_escalation_audits_previous_integrity_hash"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_escalation_audits_previous_integrity_hash ON escalation_audits (previous_integrity_hash)"))

# Closure Confirmations Table Migrations
if inspector.has_table("closure_confirmations"):
if not column_exists("closure_confirmations", "integrity_hash"):
conn.execute(text("ALTER TABLE closure_confirmations ADD COLUMN integrity_hash VARCHAR"))
logger.info("Added integrity_hash column to closure_confirmations")

if not column_exists("closure_confirmations", "previous_integrity_hash"):
conn.execute(text("ALTER TABLE closure_confirmations ADD COLUMN previous_integrity_hash VARCHAR"))
logger.info("Added previous_integrity_hash column to closure_confirmations")

if not index_exists("closure_confirmations", "ix_closure_confirmations_previous_integrity_hash"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_closure_confirmations_previous_integrity_hash ON closure_confirmations (previous_integrity_hash)"))

# Resolution Proof Tokens Table Migrations
if inspector.has_table("resolution_proof_tokens"):
if not column_exists("resolution_proof_tokens", "nonce"):
Expand Down
6 changes: 5 additions & 1 deletion backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,11 @@ class ClosureConfirmation(Base):
confirmation_type = Column(String, nullable=False) # 'confirmed', 'disputed'
reason = Column(Text, nullable=True) # Optional reason for dispute
created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc))


# Blockchain integrity fields
integrity_hash = Column(String, nullable=True)
previous_integrity_hash = Column(String, nullable=True, index=True)

# Relationship
grievance = relationship("Grievance", back_populates="closure_confirmations")

Expand Down
75 changes: 70 additions & 5 deletions backend/routers/grievances.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session, joinedload
from sqlalchemy.orm import Session, joinedload, selectinload
from sqlalchemy import func, case
from typing import List, Optional
import os
Expand Down Expand Up @@ -36,10 +36,13 @@ def get_grievances(
offset: int = Query(0, ge=0, description="Number of results to skip"),
db: Session = Depends(get_db)
):
"""Get list of grievances with escalation history"""
"""
Get list of grievances with escalation history.
Optimized: Uses selectinload for audit_logs to avoid Cartesian product and improve O(N) fetching.
"""
try:
query = db.query(Grievance).options(
joinedload(Grievance.audit_logs),
selectinload(Grievance.audit_logs),
joinedload(Grievance.jurisdiction)
)

Expand Down Expand Up @@ -92,10 +95,13 @@ def get_grievances(

@router.get("/grievances/{grievance_id}", response_model=GrievanceSummaryResponse)
def get_grievance(grievance_id: int, db: Session = Depends(get_db)):
"""Get detailed grievance information with escalation history"""
"""
Get detailed grievance information with escalation history.
Optimized: Uses selectinload for audit_logs for consistent fetching performance.
"""
try:
grievance = db.query(Grievance).options(
joinedload(Grievance.audit_logs),
selectinload(Grievance.audit_logs),
joinedload(Grievance.jurisdiction)
).filter(Grievance.id == grievance_id).first()

Expand Down Expand Up @@ -560,3 +566,62 @@ def verify_grievance_blockchain(
except Exception as e:
logger.error(f"Error verifying grievance blockchain for {grievance_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to verify grievance integrity")

@router.get("/closure-confirmation/{confirmation_id}/blockchain-verify", response_model=BlockchainVerificationResponse)
def verify_closure_confirmation_blockchain(
confirmation_id: int,
db: Session = Depends(get_db)
):
"""
Verify the cryptographic integrity of a closure confirmation using blockchain-style chaining.
Optimized: Uses previous_integrity_hash column for O(1) verification.
"""
try:
confirmation = db.query(
ClosureConfirmation.grievance_id,
ClosureConfirmation.user_email,
ClosureConfirmation.confirmation_type,
ClosureConfirmation.integrity_hash,
ClosureConfirmation.previous_integrity_hash
).filter(ClosureConfirmation.id == confirmation_id).first()

if not confirmation:
raise HTTPException(status_code=404, detail="Closure confirmation not found")

# Determine previous hash (O(1) from stored column)
prev_hash = confirmation.previous_integrity_hash or ""

# Recompute hash based on current data and previous hash
# Chaining logic: hash(grievance_id|user_email|confirmation_type|prev_hash)
hash_content = f"{confirmation.grievance_id}|{confirmation.user_email}|{confirmation.confirmation_type}|{prev_hash}"

secret_key = get_auth_config().secret_key
computed_hash = hmac.new(
secret_key.encode('utf-8'),
hash_content.encode('utf-8'),
hashlib.sha256
).hexdigest()

if confirmation.integrity_hash is None:
is_valid = False
message = "No integrity hash present for this confirmation; cryptographic integrity cannot be verified."
else:
is_valid = hmac.compare_digest(computed_hash, confirmation.integrity_hash)
message = (
"Integrity verified. This closure confirmation is cryptographically sealed."
if is_valid
else "Integrity check failed! The confirmation data does not match its cryptographic seal."
)

return BlockchainVerificationResponse(
is_valid=is_valid,
current_hash=confirmation.integrity_hash,
computed_hash=computed_hash,
message=message
)
Comment thread
RohanExploit marked this conversation as resolved.

except HTTPException:
raise
except Exception as e:
logger.error(f"Error verifying closure confirmation blockchain for {confirmation_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to verify confirmation integrity")
Loading