-
Notifications
You must be signed in to change notification settings - Fork 35
β‘ Bolt: O(1) Resolution Evidence Verification & Blockchain Chaining #625
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,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")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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: Same issue: use Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("Added valid_until column to resolution_proof_tokens") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+224
to
+230
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. π§© Analysis chainπ Script executed: #!/bin/bash
# Check what database type is configured in this project
rg -n 'DATABASE_URL|postgresql|sqlite' --type=py -C2Repository: RohanExploit/VishwaGuru Length of output: 4228 π Script executed: # Check existing migrations in init_db.py to see what types are used
head -60 backend/init_db.py | cat -n
# Also check around line 52 mentioned in the scratchpad for comparison
sed -n '45,60p' backend/init_db.py | cat -n
# Check the full list of columns being added to understand patterns
rg -n "ALTER TABLE|ADD COLUMN" backend/init_db.py -A1 | head -40Repository: RohanExploit/VishwaGuru Length of output: 5897 π Script executed: # Check main.py around line 89 to verify if migrations are disabled
sed -n '85,95p' backend/main.py | cat -nRepository: RohanExploit/VishwaGuru Length of output: 802 π Script executed: # Get the actual lines 224-230 from init_db.py
sed -n '224,230p' backend/init_db.py | cat -nRepository: RohanExploit/VishwaGuru Length of output: 667 π Script executed: # Check if there are any existing datetime columns or timestamp handling in models
rg -n "DATETIME|TIMESTAMP|datetime|timestamp" backend/models.py -B2 -A2 | head -50Repository: RohanExploit/VishwaGuru Length of output: 2466 π Script executed: # Check database.py for any datetime handling
rg -n "DATETIME|TIMESTAMP|datetime|timestamp" backend/database.pyRepository: RohanExploit/VishwaGuru Length of output: 49 π Script executed: # Verify SQLite compatibility with TIMESTAMP
python3 << 'EOF'
# Check if TIMESTAMP is valid in both SQLite and PostgreSQL
# This is general database knowledge, but let's verify behavior
# For SQLite: TIMESTAMP is accepted as a type affinity
# For PostgreSQL: TIMESTAMP is the standard datetime type
# Let's check if there's any type conversion logic in the codebase
import subprocess
result = subprocess.run(['rg', '-n', 'DateTime|TIMESTAMP|DATETIME', 'backend/', '-t', 'py'],
capture_output=True, text=True)
print("DateTime/TIMESTAMP references:")
print(result.stdout[:2000] if result.stdout else "No matches")
EOFRepository: RohanExploit/VishwaGuru Length of output: 2098 π Script executed: # Check the full init_db.py to understand all datetime column migrations
rg -n "valid_from|valid_until" backend/init_db.py -B2 -A2Repository: RohanExploit/VishwaGuru Length of output: 855 π Script executed: # Find all ALTER TABLE ADD COLUMN statements with datetime types in init_db.py
rg -n "ADD COLUMN.*DATETIME|ADD COLUMN.*TIMESTAMP" backend/init_db.pyRepository: RohanExploit/VishwaGuru Length of output: 293 Change The code uses Fix for PostgreSQL compatibility 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"))
+ conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_from TIMESTAMP"))
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"))
+ conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_until TIMESTAMP"))
logger.info("Added valid_until column to resolution_proof_tokens")π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
+209
to
+234
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. π§© Analysis chainπ Script executed: #!/bin/bash
# Verify the current state of migrate_db() in main.py
rg -n 'migrate_db' backend/main.py -C3Repository: RohanExploit/VishwaGuru Length of output: 953 π Script executed: rg -n 'def submit_evidence' backend/ -A 20Repository: RohanExploit/VishwaGuru Length of output: 1540 π Script executed: rg -n 'integrity_hash|previous_integrity_hash|valid_from|valid_until' backend/ --type pyRepository: RohanExploit/VishwaGuru Length of output: 10464 π Script executed: rg -n 'def submit_evidence' backend/resolution_proof_service.py -A 100Repository: RohanExploit/VishwaGuru Length of output: 4559 π Script executed: rg -n 'class ResolutionEvidence' backend/models.py -A 20Repository: RohanExploit/VishwaGuru Length of output: 1232 π Script executed: rg -n 'class ResolutionEvidence' backend/models.py -A 35Repository: RohanExploit/VishwaGuru Length of output: 2014 π Script executed: sed -n '385,410p' backend/resolution_proof_service.pyRepository: RohanExploit/VishwaGuru Length of output: 984 π Script executed: grep -n "resolution_proof_tokens" backend/models.py | head -20Repository: RohanExploit/VishwaGuru Length of output: 208 π Script executed: sed -n '299,320p' backend/models.pyRepository: RohanExploit/VishwaGuru Length of output: 1228 Re-enable migrations in The Either uncomment π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Backfill new columns for existing resolution_proof_tokens rows | |
| # Use existing generated_at/expires_at for legacy tokens where available. | |
| if column_exists("resolution_proof_tokens", "valid_from") and column_exists("resolution_proof_tokens", "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 | |
| """)) | |
| if column_exists("resolution_proof_tokens", "valid_until") and column_exists("resolution_proof_tokens", "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 | |
| """)) | |
| if column_exists("resolution_proof_tokens", "nonce"): | |
| # Generate a nonce for legacy rows that do not have one. | |
| # This expression uses PostgreSQL built-ins to derive a pseudo-random string. | |
| conn.execute(text(""" | |
| UPDATE resolution_proof_tokens | |
| SET nonce = md5(random()::text || clock_timestamp()::text) | |
| WHERE nonce IS NULL | |
| """)) |
| 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 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) | ||||||||||
|
Comment on lines
+307
to
+308
|
||||||||||
| valid_from = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc)) | |
| valid_until = Column(DateTime, nullable=True) | |
| valid_from = Column(DateTime, nullable=False, default=lambda: datetime.datetime.now(datetime.timezone.utc)) | |
| valid_until = Column(DateTime, nullable=False, default=lambda: datetime.datetime.now(datetime.timezone.utc)) |
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: valid_until is nullable in the model, but if it's used unconditionally in token validation or signature payloads (e.g., calling .isoformat() or checking .tzinfo), existing rows with NULL after migration will trigger AttributeError at runtime. Either make it non-nullable with a sensible default, or add a backfill strategy during migration.
Prompt for AI agents
Check if this issue is valid β if so, understand the root cause and fix it. At backend/models.py, line 308:
<comment>`valid_until` is nullable in the model, but if it's used unconditionally in token validation or signature payloads (e.g., calling `.isoformat()` or checking `.tzinfo`), existing rows with `NULL` after migration will trigger `AttributeError` at runtime. Either make it non-nullable with a sensible default, or add a backfill strategy during migration.</comment>
<file context>
@@ -300,7 +304,10 @@ class ResolutionProofToken(Base):
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)
</file context>
| 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__) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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 stores a single global Consider caching both the last record ID and hash, and re-validating against the DB tail within the same transaction before using the cached value. Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+373
to
+380
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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") | |
| # Performance Boost with safety: cache DB tail (id + integrity_hash) and | |
| # validate it against the current DB state to avoid chaining to stale hashes. | |
| cache_entry = resolution_last_hash_cache.get("last_tail") | |
| cached_id: Optional[int] = None | |
| cached_hash: Optional[str] = None | |
| # Backward compatibility: older cache may store only a string hash under this key. | |
| if isinstance(cache_entry, dict): | |
| cached_id = cache_entry.get("id") | |
| cached_hash = cache_entry.get("hash") | |
| elif isinstance(cache_entry, str): | |
| cached_hash = cache_entry or "" | |
| # Always read the current DB tail to validate the cache. | |
| db_tail: Optional[Tuple[int, Optional[str]]] = db.query( | |
| ResolutionEvidence.id, | |
| ResolutionEvidence.integrity_hash | |
| ).order_by(ResolutionEvidence.id.desc()).first() | |
| if db_tail: | |
| db_last_id, db_last_hash = db_tail | |
| db_last_hash = db_last_hash or "" | |
| else: | |
| db_last_id, db_last_hash = None, "" | |
| if cached_id is not None and cached_id == db_last_id and cached_hash == db_last_hash: | |
| # Cache is in sync with DB tail; safe to use. | |
| prev_hash = cached_hash or "" | |
| else: | |
| # Cache miss, mismatch, or legacy format; use DB tail and refresh cache. | |
| prev_hash = db_last_hash | |
| resolution_last_hash_cache.set( | |
| data={"id": db_last_id, "hash": prev_hash}, | |
| key="last_tail", | |
| ) |
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: Float-to-string interpolation via f-string ({gps_latitude}|{gps_longitude}) can produce different representations at submission time vs. after a database round-trip (e.g., 34.0 vs 34.00000000000001). This will cause false integrity-check failures in the blockchain-verify endpoint. Use fixed-precision formatting (e.g., f"{gps_latitude:.8f}") or Decimal quantization for canonical encoding.
Prompt for AI agents
Check if this issue is valid β if so, understand the root cause and fix it. At backend/resolution_proof_service.py, line 382:
<comment>Float-to-string interpolation via f-string (`{gps_latitude}|{gps_longitude}`) can produce different representations at submission time vs. after a database round-trip (e.g., `34.0` vs `34.00000000000001`). This will cause false integrity-check failures in the blockchain-verify endpoint. Use fixed-precision formatting (e.g., `f"{gps_latitude:.8f}"`) or `Decimal` quantization for canonical encoding.</comment>
<file context>
@@ -368,6 +369,19 @@ def submit_evidence(
+ 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)
+
</file context>
| hash_content = f"{evidence_hash}|{gps_latitude}|{gps_longitude}|{prev_hash}" | |
| hash_content = f"{evidence_hash}|{gps_latitude:.8f}|{gps_longitude:.8f}|{prev_hash}" |
Copilot
AI
Apr 2, 2026
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.
The integrity hash payload is built via f-string interpolation of gps_latitude/gps_longitude floats. Float string representations can differ between the value at submission time and the value round-tripped through the DB, causing false integrity failures. Use a canonical encoding for numeric fields (e.g., fixed precision formatting or Decimal quantization) in both submit_evidence and blockchain verification.
Copilot
AI
Apr 2, 2026
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.
The chaining payload only includes evidence_hash|gps_lat|gps_lon|prev_hash, so changes to other persisted fields (e.g., capture_timestamp, device_fingerprint_hash, server_signature, metadata_bundle) will not be detected by this blockchain seal. If the intent is βrecord integrityβ, consider hashing a canonical form of the full immutable evidence bundle (or at least include the server_signature/metadata fields) to prevent undetected tampering of those columns.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Comment on lines
+227
to
+255
|
||
|
|
||
| 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) | ||
|
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: Use Prompt for AI agents |
||
| 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." | ||
| ) | ||
|
Comment on lines
+261
to
+267
|
||
| 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") | ||
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.
P2: Use
TIMESTAMPinstead ofDATETIMEfor cross-database compatibility. The project supports both PostgreSQL (viaDATABASE_URL) and SQLite.DATETIMEis a SQLite-specific type; PostgreSQL requiresTIMESTAMP. Other migrations in this file use portable types.Prompt for AI agents