From 6fa04e5e9f327d80b62610acc92de09185dfe840 Mon Sep 17 00:00:00 2001 From: RohanExploit <178623867+RohanExploit@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:08:44 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20O(1)=20Resolution=20Evidenc?= =?UTF-8?q?e=20Verification=20&=20Blockchain=20Chaining?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented HMAC-SHA256 hash chaining for ResolutionEvidence records. - Optimized verify_evidence to use O(1) SQLAlchemy queries (.count() and .first()) instead of loading all records. - Added resolution_last_hash_cache for O(1) record creation. - Added O(1) blockchain-verify endpoint for resolution evidence. - Synchronized ResolutionProofToken model with test suite expectations (nonce, valid_from, valid_until). - Updated init_db.py with necessary migrations for new columns and indexes. --- .jules/bolt.md | 4 ++ backend/cache.py | 1 + backend/init_db.py | 27 +++++++++++++ backend/models.py | 7 ++++ backend/resolution_proof_service.py | 35 ++++++++++++++--- backend/routers/resolution_proof.py | 60 +++++++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 6 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 0a0c3c3e..21728a07 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -61,3 +61,7 @@ ## 2025-02-13 - API Route Prefix Consistency **Learning:** Inconsistent application of `/api` prefixes between `main.py` router mounting and test suite request paths can lead to 404 errors during testing, even if the logic is correct. This is especially prevalent when multiple agents work on the same codebase with different assumptions about global prefixes. **Action:** Always verify that `app.include_router` in `backend/main.py` uses `prefix="/api"` if the test suite (e.g., `tests/test_blockchain.py`) expects it. If a router is mounted without a prefix, ensure tests are updated or the prefix is added to `main.py` to maintain repository-wide consistency. + +## 2026-02-15 - Implicit Model/Test Contracts +**Learning:** Discrepancies between model definitions and existing test suite expectations (e.g., `tests/test_resolution_proof.py` expecting `nonce` and `valid_from` columns on `ResolutionProofToken`) cause silent failures during optimization tasks. +**Action:** Before optimizing a service, verify that the underlying models fully satisfy the schemas and test assertions. Explicitly synchronize migrations and models when missing attributes are discovered during test execution. diff --git a/backend/cache.py b/backend/cache.py index 22bcc68d..450ca638 100644 --- a/backend/cache.py +++ b/backend/cache.py @@ -180,4 +180,5 @@ def invalidate(self): blockchain_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1) grievance_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1) visit_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=2) +resolution_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1) user_issues_cache = ThreadSafeCache(ttl=300, max_size=50) # 5 minutes TTL diff --git a/backend/init_db.py b/backend/init_db.py index abc59540..56f2555a 100644 --- a/backend/init_db.py +++ b/backend/init_db.py @@ -206,6 +206,33 @@ def index_exists(table, index_name): if not index_exists("field_officer_visits", "ix_field_officer_visits_previous_visit_hash"): conn.execute(text("CREATE INDEX IF NOT EXISTS ix_field_officer_visits_previous_visit_hash ON field_officer_visits (previous_visit_hash)")) + # Resolution Evidence Table Migrations + if inspector.has_table("resolution_evidence"): + if not column_exists("resolution_evidence", "integrity_hash"): + conn.execute(text("ALTER TABLE resolution_evidence ADD COLUMN integrity_hash VARCHAR")) + logger.info("Added integrity_hash column to resolution_evidence") + + if not column_exists("resolution_evidence", "previous_integrity_hash"): + conn.execute(text("ALTER TABLE resolution_evidence ADD COLUMN previous_integrity_hash VARCHAR")) + logger.info("Added previous_integrity_hash column to resolution_evidence") + + if not index_exists("resolution_evidence", "ix_resolution_evidence_previous_integrity_hash"): + conn.execute(text("CREATE INDEX IF NOT EXISTS ix_resolution_evidence_previous_integrity_hash ON resolution_evidence (previous_integrity_hash)")) + + # Resolution Proof Tokens Table Migrations + if inspector.has_table("resolution_proof_tokens"): + if not column_exists("resolution_proof_tokens", "valid_from"): + conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_from DATETIME")) + logger.info("Added valid_from column to resolution_proof_tokens") + + if not column_exists("resolution_proof_tokens", "valid_until"): + conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_until DATETIME")) + logger.info("Added valid_until column to resolution_proof_tokens") + + if not column_exists("resolution_proof_tokens", "nonce"): + conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN nonce VARCHAR")) + logger.info("Added nonce column to resolution_proof_tokens") + logger.info("Database migration check completed successfully.") except Exception as e: diff --git a/backend/models.py b/backend/models.py index 3e8f7545..85b7901a 100644 --- a/backend/models.py +++ b/backend/models.py @@ -287,6 +287,10 @@ class ResolutionEvidence(Base): server_signature = Column(String, nullable=True) verification_status = Column(Enum(VerificationStatus), default=VerificationStatus.PENDING) + # Blockchain integrity fields + integrity_hash = Column(String, nullable=True) + previous_integrity_hash = Column(String, nullable=True, index=True) + # Relationships grievance = relationship("Grievance", back_populates="resolution_evidence") audit_logs = relationship("EvidenceAuditLog", back_populates="evidence") @@ -300,7 +304,10 @@ class ResolutionProofToken(Base): token_id = Column(String, unique=True, index=True, nullable=True) # UUID string authority_email = Column(String, nullable=True) generated_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc)) + valid_from = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc)) + valid_until = Column(DateTime, nullable=True) expires_at = Column(DateTime, nullable=False) + nonce = Column(String, nullable=True) is_used = Column(Boolean, default=False) used_at = Column(DateTime, nullable=True) geofence_latitude = Column(Float, nullable=True) diff --git a/backend/resolution_proof_service.py b/backend/resolution_proof_service.py index 66fa4576..d38f43c5 100644 --- a/backend/resolution_proof_service.py +++ b/backend/resolution_proof_service.py @@ -25,6 +25,7 @@ EvidenceAuditLog, VerificationStatus, GrievanceStatus ) from backend.config import get_config +from backend.cache import resolution_last_hash_cache logger = logging.getLogger(__name__) @@ -368,6 +369,19 @@ def submit_evidence( bundle_str = json.dumps(metadata_bundle, sort_keys=True) server_signature = ResolutionProofService._sign_payload(bundle_str) + # 5a. Blockchain feature: calculate integrity hash for the evidence record + # Performance Boost: Use thread-safe cache to eliminate DB query for last hash + prev_hash = resolution_last_hash_cache.get("last_hash") + if prev_hash is None: + # Cache miss: Fetch only the last hash from DB + prev_record = db.query(ResolutionEvidence.integrity_hash).order_by(ResolutionEvidence.id.desc()).first() + prev_hash = prev_record[0] if prev_record and prev_record[0] else "" + resolution_last_hash_cache.set(data=prev_hash, key="last_hash") + + # Chaining logic: HMAC-SHA256(evidence_hash|gps_lat|gps_lon|prev_hash) + hash_content = f"{evidence_hash}|{gps_latitude}|{gps_longitude}|{prev_hash}" + integrity_hash = ResolutionProofService._sign_payload(hash_content) + # 6. Create evidence record evidence = ResolutionEvidence( grievance_id=token.grievance_id, @@ -380,6 +394,8 @@ def submit_evidence( metadata_bundle=metadata_bundle, server_signature=server_signature, verification_status=VerificationStatus.VERIFIED, + integrity_hash=integrity_hash, + previous_integrity_hash=prev_hash ) db.add(evidence) @@ -391,6 +407,9 @@ def submit_evidence( db.commit() db.refresh(evidence) + # Update cache for next evidence entry - ONLY after successful commit to prevent cache poisoning + resolution_last_hash_cache.set(data=integrity_hash, key="last_hash") + # 8. Create audit log ResolutionProofService._create_audit_log( evidence_id=evidence.id, @@ -435,11 +454,13 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]: Returns: Verification result dictionary """ - evidence_records = db.query(ResolutionEvidence).filter( + # Optimized: Use count() for total and fetch only the latest record + # This is O(1) in memory instead of loading all records with .all() + evidence_count = db.query(ResolutionEvidence).filter( ResolutionEvidence.grievance_id == grievance_id - ).all() + ).count() - if not evidence_records: + if evidence_count == 0: return { "grievance_id": grievance_id, "is_verified": False, @@ -452,8 +473,10 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]: "message": "No resolution evidence found for this grievance" } - # Use the most recent evidence - evidence = evidence_records[-1] + # Fetch ONLY the most recent evidence record + evidence = db.query(ResolutionEvidence).filter( + ResolutionEvidence.grievance_id == grievance_id + ).order_by(ResolutionEvidence.id.desc()).first() # Re-verify the server signature bundle_str = json.dumps(evidence.metadata_bundle, sort_keys=True) @@ -494,7 +517,7 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]: "location_match": location_match, "evidence_integrity": signature_valid, "evidence_hash": evidence.evidence_hash, - "evidence_count": len(evidence_records), + "evidence_count": evidence_count, "message": ( "Resolution verified with cryptographic proof" if is_verified diff --git a/backend/routers/resolution_proof.py b/backend/routers/resolution_proof.py index 25f1c5de..ec67d825 100644 --- a/backend/routers/resolution_proof.py +++ b/backend/routers/resolution_proof.py @@ -20,6 +20,7 @@ SubmitEvidenceRequest, EvidenceResponse, VerificationResponse, AuditTrailResponse, DuplicateCheckResponse, + BlockchainVerificationResponse ) logger = logging.getLogger(__name__) @@ -217,3 +218,62 @@ def flag_duplicate_evidence( except Exception as e: logger.error(f"Error checking duplicates: {e}", exc_info=True) raise HTTPException(status_code=500, detail="Failed to check for duplicates") + + +# ============================================================================ +# BLOCKCHAIN VERIFICATION (O(1)) +# ============================================================================ + +@router.get("/{evidence_id}/blockchain-verify", response_model=BlockchainVerificationResponse) +def verify_evidence_blockchain( + evidence_id: int, + db: Session = Depends(get_db) +): + """ + Verify the cryptographic integrity of an individual resolution evidence record. + Optimized: Uses previous_integrity_hash column for O(1) verification. + """ + try: + from backend.models import ResolutionEvidence + evidence = db.query( + ResolutionEvidence.evidence_hash, + ResolutionEvidence.gps_latitude, + ResolutionEvidence.gps_longitude, + ResolutionEvidence.integrity_hash, + ResolutionEvidence.previous_integrity_hash + ).filter(ResolutionEvidence.id == evidence_id).first() + + if not evidence: + raise HTTPException(status_code=404, detail="Evidence not found") + + # Determine previous hash (O(1) from stored column) + prev_hash = evidence.previous_integrity_hash or "" + + # Recompute hash based on current data and previous hash + # Chaining logic: HMAC-SHA256(evidence_hash|gps_lat|gps_lon|prev_hash) + hash_content = f"{evidence.evidence_hash}|{evidence.gps_latitude}|{evidence.gps_longitude}|{prev_hash}" + computed_hash = ResolutionProofService._sign_payload(hash_content) + + if evidence.integrity_hash is None: + # Legacy or unsealed record + is_valid = False + message = "No integrity hash present; cryptographic integrity cannot be verified." + else: + is_valid = (computed_hash == evidence.integrity_hash) + message = ( + "Integrity verified. This evidence record is cryptographically sealed." + if is_valid + else "Integrity check failed! The evidence data does not match its cryptographic seal." + ) + return BlockchainVerificationResponse( + is_valid=is_valid, + current_hash=evidence.integrity_hash, + computed_hash=computed_hash, + message=message + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error verifying evidence blockchain for {evidence_id}: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="Failed to verify evidence integrity")