-
Notifications
You must be signed in to change notification settings - Fork 35
⚡ Bolt: Resolution Evidence Blockchain & Performance Optimization #629
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+225
to
+236
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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") | |
| has_valid_from = column_exists("resolution_proof_tokens", "valid_from") | |
| if not has_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") | |
| has_valid_from = True | |
| has_valid_until = column_exists("resolution_proof_tokens", "valid_until") | |
| if not has_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") | |
| has_valid_until = True | |
| has_nonce = column_exists("resolution_proof_tokens", "nonce") | |
| if not has_nonce: | |
| conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN nonce VARCHAR")) | |
| logger.info("Added nonce column to resolution_proof_tokens") | |
| has_nonce = True | |
| has_generated_at = column_exists("resolution_proof_tokens", "generated_at") | |
| has_expires_at = column_exists("resolution_proof_tokens", "expires_at") | |
| if has_valid_from and has_generated_at: | |
| conn.execute(text( | |
| "UPDATE resolution_proof_tokens " | |
| "SET valid_from = generated_at " | |
| "WHERE valid_from IS NULL AND generated_at IS NOT NULL" | |
| )) | |
| logger.info("Backfilled valid_from from generated_at for legacy resolution_proof_tokens rows") | |
| if has_valid_until and has_expires_at: | |
| conn.execute(text( | |
| "UPDATE resolution_proof_tokens " | |
| "SET valid_until = expires_at " | |
| "WHERE valid_until IS NULL AND expires_at IS NOT NULL" | |
| )) | |
| logger.info("Backfilled valid_until from expires_at for legacy resolution_proof_tokens rows") | |
| if has_nonce: | |
| if column_exists("resolution_proof_tokens", "token"): | |
| conn.execute(text( | |
| "UPDATE resolution_proof_tokens " | |
| "SET nonce = token " | |
| "WHERE nonce IS NULL AND token IS NOT NULL" | |
| )) | |
| logger.info("Backfilled nonce from token for legacy resolution_proof_tokens rows") | |
| else: | |
| logger.warning( | |
| "resolution_proof_tokens.nonce exists but no legacy source column was found for backfill; " | |
| "legacy rows may require explicit compatibility handling during validation." | |
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Comment on lines
+306
to
310
|
||
| is_used = Column(Boolean, default=False) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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") | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
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. P1: The cache-based Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| 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") | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+374
to
+381
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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") | |
| # Treat the cache as a hint only: validate against the current DB tail | |
| cached_tail = resolution_last_hash_cache.get("last_evidence_tail") | |
| last_record = ( | |
| db.query(ResolutionEvidence.id, ResolutionEvidence.integrity_hash) | |
| .order_by(ResolutionEvidence.id.desc()) | |
| .first() | |
| ) | |
| db_tail = ( | |
| (last_record[0], last_record[1] or "") | |
| if last_record | |
| else (None, "") | |
| ) | |
| if cached_tail != db_tail: | |
| resolution_last_hash_cache.set(data=db_tail, key="last_evidence_tail") | |
| prev_hash = db_tail[1] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 "" | ||
|
|
||
|
Comment on lines
+233
to
+250
|
||
| # 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") | ||
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.
Handle legacy
resolution_proof_tokensrows during this migration.Lines 225-234 only add
valid_from,valid_until, andnonceas nullable columns.ResolutionProofService.validate_token()now treats those fields as required when checking expiry and rebuilding the HMAC, so any still-live token created before this migration can start failing validation or blow up onvalid_until.tzinfo. Add a data migration that backfillsvalid_from/valid_untilfrom the legacy timestamps and explicitly expires or regenerates rows that have no nonce.🤖 Prompt for AI Agents