From 011f37545d1a7a341bac0823a8817c184950b915 Mon Sep 17 00:00:00 2001 From: RohanExploit <178623867+RohanExploit@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:08:51 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Implement=20blockchain=20fo?= =?UTF-8?q?r=20resolution=20evidence=20with=20O(1)=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `integrity_hash` and `previous_integrity_hash` columns to `ResolutionEvidence` model. - Implemented cryptographic chaining (HMAC-SHA256) in `ResolutionProofService.submit_evidence`. - Optimized `ResolutionProofService.verify_evidence` to use `.count()` and fetching only the latest record instead of materializing all records. - Integrated `resolution_last_hash_cache` (ThreadSafeCache) for O(1) previous hash retrieval during submission. - Added `/api/resolution-proof/{evidence_id}/blockchain-verify` endpoint for O(1) single-record integrity validation. - Fixed `ResolutionProofToken` model to explicitly store `valid_from`, `valid_until`, and `nonce` for secure signing and satisfy `expires_at` NOT NULL constraint. - Updated `backend/init_db.py` with required schema migrations. --- .jules/bolt.md | 8 ++++ backend/cache.py | 1 + backend/init_db.py | 28 ++++++++++++++ backend/models.py | 7 ++++ backend/resolution_proof_service.py | 35 ++++++++++++++--- backend/routers/resolution_proof.py | 60 ++++++++++++++++++++++++++++- 6 files changed, 132 insertions(+), 7 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 0a0c3c3e..017c357c 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -61,3 +61,11 @@ ## 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-14 - Cache Consistency in Blockchain Chaining +**Learning:** When using in-memory caches (like `ThreadSafeCache`) to store the "last hash" for blockchain chaining, updating the cache *before* a successful database commit can lead to cache poisoning if the transaction fails. Subsequent records would then chain to a hash that doesn't exist in the database. +**Action:** Always update the "last hash" cache ONLY after a successful `db.commit()`. Additionally, when retrieving the previous hash, perform a quick check against the database to ensure the cached hash matches the actual last record, providing a fail-safe against cache inconsistency in multi-worker environments. + +## 2026-02-14 - Optimized Evidence Verification +**Learning:** Materializing all evidence records for a grievance using `.all()` just to get the count or the latest record is inefficient, especially as the system scales. +**Action:** Use `.count()` for existence checks and `.order_by(Model.id.desc()).first()` to fetch only the latest record. This reduces memory pressure and database transfer overhead. diff --git a/backend/cache.py b/backend/cache.py index 22bcc68d..01775f33 100644 --- a/backend/cache.py +++ b/backend/cache.py @@ -179,5 +179,6 @@ def invalidate(self): user_upload_cache = ThreadSafeCache(ttl=3600, max_size=1000) # 1 hour TTL for upload limits blockchain_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1) grievance_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1) +resolution_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=1) visit_last_hash_cache = ThreadSafeCache(ttl=3600, max_size=2) 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..943a7a90 100644 --- a/backend/init_db.py +++ b/backend/init_db.py @@ -206,6 +206,34 @@ 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)")) + logger.info("Created index ix_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..7865e1a0 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 chaining + 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") @@ -299,6 +303,9 @@ class ResolutionProofToken(Base): token = Column(String, unique=True, index=True, nullable=True) token_id = Column(String, unique=True, index=True, nullable=True) # UUID string authority_email = Column(String, nullable=True) + valid_from = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc)) + valid_until = Column(DateTime, nullable=True) + nonce = Column(String, nullable=True) generated_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc)) expires_at = Column(DateTime, nullable=False) is_used = Column(Boolean, default=False) diff --git a/backend/resolution_proof_service.py b/backend/resolution_proof_service.py index 66fa4576..2067a37f 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__) @@ -197,6 +198,7 @@ def generate_proof_token( geofence_radius_meters=geofence_radius, valid_from=now, valid_until=valid_until, + expires_at=valid_until, # Explicitly set for DB constraint/legacy compatibility nonce=nonce, token_signature=signature, is_used=False, @@ -368,6 +370,19 @@ def submit_evidence( bundle_str = json.dumps(metadata_bundle, sort_keys=True) server_signature = ResolutionProofService._sign_payload(bundle_str) + # 5b. Implement cryptographic chaining (Issue #BLOCKCHAIN-003) + # Performance Boost: Use thread-safe cache for O(1) last hash retrieval + prev_hash = resolution_last_hash_cache.get("last_hash") + if prev_hash is None: + # Cache miss: fetch ONLY the last hash from DB + last_record = db.query(ResolutionEvidence.integrity_hash).order_by(ResolutionEvidence.id.desc()).first() + prev_hash = last_record[0] if last_record and last_record[0] else "" + resolution_last_hash_cache.set(data=prev_hash, key="last_hash") + + # Chaining logic: hash(evidence_hash|token_id|prev_hash) + chain_payload = f"{evidence_hash}|{token.token_id}|{prev_hash}" + integrity_hash = ResolutionProofService._sign_payload(chain_payload) + # 6. Create evidence record evidence = ResolutionEvidence( grievance_id=token.grievance_id, @@ -380,6 +395,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 +408,9 @@ def submit_evidence( db.commit() db.refresh(evidence) + # Update cache AFTER successful commit to prevent 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 +455,12 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]: Returns: Verification result dictionary """ - evidence_records = db.query(ResolutionEvidence).filter( + # Performance Boost: Use .count() for existence check instead of materializing all records + 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] + # Performance Boost: 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..958f8d4d 100644 --- a/backend/routers/resolution_proof.py +++ b/backend/routers/resolution_proof.py @@ -14,12 +14,13 @@ from sqlalchemy.orm import Session from backend.database import get_db +from backend.models import ResolutionEvidence from backend.resolution_proof_service import ResolutionProofService from backend.schemas import ( GenerateRPTRequest, RPTResponse, SubmitEvidenceRequest, EvidenceResponse, VerificationResponse, AuditTrailResponse, - DuplicateCheckResponse, + DuplicateCheckResponse, BlockchainVerificationResponse ) logger = logging.getLogger(__name__) @@ -217,3 +218,60 @@ 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") + + +@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 a resolution evidence record using blockchain-style chaining. + Optimized: Uses previous_integrity_hash column for O(1) verification. + """ + try: + evidence = db.query( + ResolutionEvidence.evidence_hash, + ResolutionEvidence.token_id, + 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 "" + + # Fetch token_id string for chaining logic consistency + from backend.models import ResolutionProofToken + token = db.query(ResolutionProofToken.token_id).filter(ResolutionProofToken.id == evidence.token_id).first() + token_id_str = token[0] if token else "" + + # Chaining logic: hash(evidence_hash|token_id|prev_hash) + chain_payload = f"{evidence.evidence_hash}|{token_id_str}|{prev_hash}" + computed_hash = ResolutionProofService._sign_payload(chain_payload) + + if evidence.integrity_hash is None: + is_valid = False + message = "No integrity hash present for this record; cryptographic integrity cannot be verified." + else: + is_valid = (computed_hash == evidence.integrity_hash) + message = ( + "Integrity verified. This resolution 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")