-
Notifications
You must be signed in to change notification settings - Fork 36
β‘ Bolt: O(1) Blockchain Integrity for Grievances #576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import hashlib | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Dict, Any, Optional, List | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sqlalchemy.orm import Session, joinedload | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import datetime, timezone, timedelta | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -14,6 +15,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.routing_service import RoutingService | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.sla_config_service import SLAConfigService | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.escalation_engine import EscalationEngine | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.cache import grievance_last_hash_cache | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class GrievanceService: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -84,6 +86,18 @@ def create_grievance(self, grievance_data: Dict[str, Any], db: Session = None) - | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Generate unique ID | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unique_id = str(uuid.uuid4())[:8].upper() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Blockchain integrity logic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev_hash = grievance_last_hash_cache.get("last_hash") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if prev_hash is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fetch 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 on lines
+90
to
+95
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev_hash = grievance_last_hash_cache.get("last_hash") | |
| if prev_hash is None: | |
| # Fetch 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") | |
| # 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 "" | |
| grievance_last_hash_cache.set(data=prev_hash, key="last_hash") | |
| grievance_last_hash_cache.set(data=db_last_id, key="last_id") | |
| else: | |
| prev_hash = cached_prev_hash or "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Serialize chain-head updates to avoid hash-chain forks under concurrency.
Line 90 (get) and Line 133 (set) are separated by non-atomic work + commit, so concurrent creates can reuse the same prev_hash and produce multiple records pointing to one predecessor.
π Suggested fix (transaction-level serialization)
+from sqlalchemy import text
...
- # Blockchain integrity logic
+ # Blockchain integrity logic
+ # Serialize chain-head updates within the DB transaction.
+ # (Use DB-equivalent lock primitive if not on Postgres.)
+ db.execute(text("SELECT pg_advisory_xact_lock(:lock_key)"), {"lock_key": 576001})
prev_hash = grievance_last_hash_cache.get("last_hash")
if prev_hash is None:
# Fetch 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")Also applies to: 132-133
π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/grievance_service.py` around lines 89 - 100, The current flow reads
grievance_last_hash_cache.get("last_hash") and later sets it after non-atomic
work, causing concurrent creates to reuse the same prev_hash; fix by performing
the read, compute, insert, and update of the chain-head atomically inside the
same DB transaction and with a lock: open a transaction (e.g.,
db.session.begin()), acquire a row-level lock (SELECT ... FOR UPDATE) on a
dedicated chain-head record or lock the Grievance table/row used for chaining,
read the last hash (replace grievance_last_hash_cache.get usage inside the
transaction), compute integrity_hash (the existing hash_content logic), insert
the new Grievance, then update the chain-head record and call
grievance_last_hash_cache.set only after commit (or update cache inside the same
transaction), ensuring all work around grievance_last_hash_cache.get/set, the
integrity_hash calculation, and the Grievance insert occurs under that
transaction/lock to prevent forks.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -126,9 +126,8 @@ async def lifespan(app: FastAPI): | |||||
| app = FastAPI( | ||||||
| title="VishwaGuru Backend", | ||||||
| description="AI-powered civic issue reporting and resolution platform", | ||||||
| version="1.0.0" | ||||||
| # Temporarily disable lifespan for local dev debugging | ||||||
| # lifespan=lifespan | ||||||
| version="1.0.0", | ||||||
| lifespan=lifespan | ||||||
| ) | ||||||
|
|
||||||
| # Add centralized exception handlers | ||||||
|
|
@@ -186,18 +185,18 @@ async def lifespan(app: FastAPI): | |||||
| os.makedirs("data/uploads", exist_ok=True) | ||||||
| app.mount("/uploads", StaticFiles(directory="data/uploads"), name="uploads") | ||||||
|
|
||||||
| # Include Modular Routers | ||||||
| app.include_router(issues.router, tags=["Issues"]) | ||||||
| app.include_router(detection.router, tags=["Detection"]) | ||||||
| app.include_router(grievances.router, tags=["Grievances"]) | ||||||
| app.include_router(utility.router, tags=["Utility"]) | ||||||
| app.include_router(auth.router, tags=["Authentication"]) | ||||||
| app.include_router(admin.router) | ||||||
| app.include_router(analysis.router, tags=["Analysis"]) | ||||||
| app.include_router(voice.router, tags=["Voice & Language"]) | ||||||
| app.include_router(field_officer.router, tags=["Field Officer Check-In"]) | ||||||
| app.include_router(hf.router, tags=["Hugging Face"]) | ||||||
| app.include_router(resolution_proof.router, tags=["Resolution Proof"]) | ||||||
| # Include Modular Routers with /api prefix | ||||||
| app.include_router(issues.router, prefix="/api", tags=["Issues"]) | ||||||
| app.include_router(detection.router, prefix="/api", tags=["Detection"]) | ||||||
| app.include_router(grievances.router, prefix="/api", tags=["Grievances"]) | ||||||
| app.include_router(utility.router, prefix="/api", tags=["Utility"]) | ||||||
| app.include_router(auth.router, prefix="/api", tags=["Authentication"]) | ||||||
| app.include_router(admin.router, prefix="/api") | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The admin router already has Prompt for AI agents
Suggested change
|
||||||
| app.include_router(analysis.router, prefix="/api", tags=["Analysis"]) | ||||||
| app.include_router(voice.router, prefix="/api", tags=["Voice & Language"]) | ||||||
| app.include_router(field_officer.router, prefix="/api", tags=["Field Officer Check-In"]) | ||||||
| app.include_router(hf.router, prefix="/api", tags=["Hugging Face"]) | ||||||
| app.include_router(resolution_proof.router, prefix="/api", tags=["Resolution Proof"]) | ||||||
|
|
||||||
| @app.get("/health") | ||||||
| def health(): | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,5 +31,3 @@ SpeechRecognition | |
| pydub | ||
| googletrans==4.0.2 | ||
| langdetect | ||
| indic-nlp-library | ||
| async_lru | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||
| import hashlib | ||||||||||||||||||||||||||||||||||||
| from datetime import datetime, timezone | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| from backend.database import get_db | ||||||||||||||||||||||||||||||||||||
|
|
@@ -15,7 +16,8 @@ | |||||||||||||||||||||||||||||||||||
| FollowGrievanceRequest, FollowGrievanceResponse, | ||||||||||||||||||||||||||||||||||||
| RequestClosureRequest, RequestClosureResponse, | ||||||||||||||||||||||||||||||||||||
| ConfirmClosureRequest, ConfirmClosureResponse, | ||||||||||||||||||||||||||||||||||||
| ClosureStatusResponse | ||||||||||||||||||||||||||||||||||||
| ClosureStatusResponse, | ||||||||||||||||||||||||||||||||||||
| BlockchainVerificationResponse | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| from backend.grievance_service import GrievanceService | ||||||||||||||||||||||||||||||||||||
| from backend.closure_service import ClosureService | ||||||||||||||||||||||||||||||||||||
|
|
@@ -436,3 +438,52 @@ def get_closure_status( | |||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||
| logger.error(f"Error getting closure status for grievance {grievance_id}: {e}", exc_info=True) | ||||||||||||||||||||||||||||||||||||
| raise HTTPException(status_code=500, detail="Failed to get closure status") | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @router.get("/grievances/{grievance_id}/blockchain-verify", response_model=BlockchainVerificationResponse) | ||||||||||||||||||||||||||||||||||||
| def verify_grievance_blockchain( | ||||||||||||||||||||||||||||||||||||
| grievance_id: int, | ||||||||||||||||||||||||||||||||||||
| db: Session = Depends(get_db) | ||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| Verify the cryptographic integrity of a grievance using blockchain-style chaining. | ||||||||||||||||||||||||||||||||||||
| Optimized: Uses previous_integrity_hash column for O(1) verification. | ||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||
| grievance = db.query( | ||||||||||||||||||||||||||||||||||||
| Grievance.unique_id, | ||||||||||||||||||||||||||||||||||||
| Grievance.category, | ||||||||||||||||||||||||||||||||||||
| Grievance.severity, | ||||||||||||||||||||||||||||||||||||
| Grievance.integrity_hash, | ||||||||||||||||||||||||||||||||||||
| Grievance.previous_integrity_hash | ||||||||||||||||||||||||||||||||||||
| ).filter(Grievance.id == grievance_id).first() | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if not grievance: | ||||||||||||||||||||||||||||||||||||
| raise HTTPException(status_code=404, detail="Grievance not found") | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Determine previous hash (O(1) from stored column) | ||||||||||||||||||||||||||||||||||||
| prev_hash = grievance.previous_integrity_hash or "" | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Recompute hash based on current data and previous hash | ||||||||||||||||||||||||||||||||||||
| # Chaining logic: hash(unique_id|category|severity|prev_hash) | ||||||||||||||||||||||||||||||||||||
| severity_value = grievance.severity.value if hasattr(grievance.severity, 'value') else grievance.severity | ||||||||||||||||||||||||||||||||||||
| hash_content = f"{grievance.unique_id}|{grievance.category}|{severity_value}|{prev_hash}" | ||||||||||||||||||||||||||||||||||||
| computed_hash = hashlib.sha256(hash_content.encode()).hexdigest() | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+464
to
+471
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| is_valid = (computed_hash == grievance.integrity_hash) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| message = "Integrity verified. This grievance record is cryptographically sealed." if is_valid \ | ||||||||||||||||||||||||||||||||||||
| else "Integrity check failed! The grievance data does not match its cryptographic seal." | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
Comment on lines
+473
to
+477
|
||||||||||||||||||||||||||||||||||||
| is_valid = (computed_hash == grievance.integrity_hash) | |
| message = "Integrity verified. This grievance record is cryptographically sealed." if is_valid \ | |
| else "Integrity check failed! The grievance data does not match its cryptographic seal." | |
| if grievance.integrity_hash is None: | |
| # Legacy or unsealed grievance: no integrity hash stored, so we cannot verify tampering. | |
| is_valid = False | |
| message = ( | |
| "No integrity hash present for this grievance; cryptographic integrity cannot be verified." | |
| ) | |
| else: | |
| is_valid = (computed_hash == grievance.integrity_hash) | |
| message = ( | |
| "Integrity verified. This grievance record is cryptographically sealed." | |
| if is_valid | |
| else "Integrity check failed! The grievance data does not match its cryptographic seal." | |
| ) |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Race condition: concurrent
create_grievancecalls read the sameprev_hashfrom the cache, producing two grievances with identicalprevious_integrity_hashand forking the chain. The cache's internal lock only guards individual get/set calls β it does not make the full read β hash β commit β update-cache sequence atomic. Wrap this section in a process-level or DB-level lock (e.g.,SELECT ... FOR UPDATEon the last row, or a threading lock around the entire block) to serialize chain extension.Prompt for AI agents