-
Notifications
You must be signed in to change notification settings - Fork 35
⚡ Bolt: O(1) Blockchain Integrity for Resolution Evidence #621
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,37 @@ 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", "expires_at"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN expires_at DATETIME")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("Added expires_at 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") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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: Migration adds Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_until DATETIME")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+228
to
+237
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. Backfill the new token metadata or keep validation backward compatible.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("Added valid_until column to resolution_proof_tokens") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Backfill newly added columns for existing rows to avoid NULLs | |
| # valid_from: prefer generated_at if present; otherwise use current timestamp | |
| if column_exists("resolution_proof_tokens", "generated_at"): | |
| conn.execute( | |
| text( | |
| """ | |
| UPDATE resolution_proof_tokens | |
| SET valid_from = generated_at | |
| WHERE valid_from IS NULL | |
| """ | |
| ) | |
| ) | |
| else: | |
| conn.execute( | |
| text( | |
| """ | |
| UPDATE resolution_proof_tokens | |
| SET valid_from = CURRENT_TIMESTAMP | |
| WHERE valid_from IS NULL | |
| """ | |
| ) | |
| ) | |
| # valid_until: prefer existing expires_at if present; otherwise use current timestamp | |
| if column_exists("resolution_proof_tokens", "expires_at"): | |
| conn.execute( | |
| text( | |
| """ | |
| UPDATE resolution_proof_tokens | |
| SET valid_until = expires_at | |
| WHERE valid_until IS NULL | |
| """ | |
| ) | |
| ) | |
| else: | |
| conn.execute( | |
| text( | |
| """ | |
| UPDATE resolution_proof_tokens | |
| SET valid_until = CURRENT_TIMESTAMP | |
| WHERE valid_until IS NULL | |
| """ | |
| ) | |
| ) | |
| # nonce: ensure non-NULL value for legacy tokens | |
| conn.execute( | |
| text( | |
| """ | |
| UPDATE resolution_proof_tokens | |
| SET nonce = 'legacy' | |
| 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") | ||
|
|
@@ -307,6 +311,9 @@ class ResolutionProofToken(Base): | |
| geofence_longitude = Column(Float, nullable=True) | ||
| geofence_radius_meters = Column(Float, default=200.0) | ||
| token_signature = Column(String, nullable=True) | ||
| nonce = Column(String, nullable=True) | ||
| valid_from = Column(DateTime, nullable=True) | ||
| valid_until = Column(DateTime, nullable=True) | ||
|
Comment on lines
311
to
+316
|
||
|
|
||
| # Relationship | ||
| grievance = relationship("Grievance", back_populates="resolution_tokens") | ||
|
|
||
| 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, | ||||||||||||||||||||||||||||||||||||||||
| nonce=nonce, | ||||||||||||||||||||||||||||||||||||||||
| token_signature=signature, | ||||||||||||||||||||||||||||||||||||||||
| is_used=False, | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -368,7 +370,20 @@ def submit_evidence( | |||||||||||||||||||||||||||||||||||||||
| bundle_str = json.dumps(metadata_bundle, sort_keys=True) | ||||||||||||||||||||||||||||||||||||||||
| server_signature = ResolutionProofService._sign_payload(bundle_str) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # 6. Create evidence record | ||||||||||||||||||||||||||||||||||||||||
| # 6. Blockchain feature: calculate integrity hash for the evidence (Issue #292) | ||||||||||||||||||||||||||||||||||||||||
| # 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: Concurrent To make this correct, the previous hash must be obtained under a serializable guarantee — e.g., a database-level Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||
| if prev_hash is None: | ||||||||||||||||||||||||||||||||||||||||
| # Cache miss: Fetch only the last hash from DB | ||||||||||||||||||||||||||||||||||||||||
| prev_evidence = db.query(ResolutionEvidence.integrity_hash).order_by(ResolutionEvidence.id.desc()).first() | ||||||||||||||||||||||||||||||||||||||||
| prev_hash = prev_evidence[0] if prev_evidence and prev_evidence[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 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_evidence = db.query(ResolutionEvidence.integrity_hash).order_by(ResolutionEvidence.id.desc()).first() | |
| prev_hash = prev_evidence[0] if prev_evidence and prev_evidence[0] else "" | |
| resolution_last_hash_cache.set(data=prev_hash, key="last_hash") | |
| # Ensure the cached last hash matches the current DB tail before using it | |
| latest_row = db.query(ResolutionEvidence.integrity_hash).order_by(ResolutionEvidence.id.desc()).first() | |
| db_last_hash = latest_row[0] if latest_row and latest_row[0] else "" | |
| cached_last_hash = resolution_last_hash_cache.get("last_hash") | |
| if cached_last_hash is None or cached_last_hash != db_last_hash: | |
| # Cache miss or stale cache: refresh from DB | |
| prev_hash = db_last_hash | |
| resolution_last_hash_cache.set(data=db_last_hash, key="last_hash") | |
| else: | |
| # Cache is in sync with DB tail | |
| prev_hash = cached_last_hash |
Copilot
AI
Apr 1, 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.
evidence.metadata_bundle is nullable (JSON column) but verification assumes it’s a dict (.get("token_id")). Legacy/partially-populated evidence rows will trigger an AttributeError and fail verification with a 500. Guard with metadata = evidence.metadata_bundle or {} (and consider failing blockchain verification gracefully if required fields are missing).
| token_uuid = evidence.metadata_bundle.get("token_id", "") | |
| metadata = evidence.metadata_bundle or {} | |
| if isinstance(metadata, dict): | |
| token_uuid = str(metadata.get("token_id", "")) | |
| else: | |
| logger.warning( | |
| "Unexpected metadata_bundle type for evidence %s: %s", | |
| getattr(evidence, "id", None), | |
| type(metadata).__name__, | |
| ) | |
| token_uuid = "" |
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: evidence.metadata_bundle is nullable (JSON column). When it's None on legacy or partially-populated rows, .get("token_id", "") will raise AttributeError, causing verification to fail with a 500. Guard with metadata = evidence.metadata_bundle or {}.
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 500:
<comment>`evidence.metadata_bundle` is nullable (JSON column). When it's `None` on legacy or partially-populated rows, `.get("token_id", "")` will raise `AttributeError`, causing verification to fail with a 500. Guard with `metadata = evidence.metadata_bundle or {}`.</comment>
<file context>
@@ -475,9 +494,18 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
+ # Verify blockchain integrity
+ prev_hash = evidence.previous_integrity_hash or ""
+ # Re-derive token_id for hash (it's in metadata_bundle)
+ token_uuid = evidence.metadata_bundle.get("token_id", "")
+ hash_content = f"{evidence.evidence_hash}|{token_uuid}|{evidence.gps_latitude}|{evidence.gps_longitude}|{prev_hash}"
+ computed_integrity_hash = ResolutionProofService._sign_payload(hash_content)
</file context>
| token_uuid = evidence.metadata_bundle.get("token_id", "") | |
| metadata = evidence.metadata_bundle or {} | |
| token_uuid = metadata.get("token_id", "") |
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 pre-chain evidence explicitly instead of marking it as tampered.
backend/models.py::ResolutionEvidence.integrity_hash and backend/models.py::ResolutionEvidence.metadata_bundle are nullable. For pre-rollout rows this branch either calls None.get("token_id") or forces blockchain_valid=False, which makes already-resolved grievances fail verification after deployment. Add a compatibility path/backfill instead of folding missing blockchain data into tamper detection.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/resolution_proof_service.py` around lines 497 - 503, The verification
currently treats missing blockchain fields as tampering and calls .get on a
nullable metadata_bundle; change the logic in ResolutionProofService._verify
(the block computing computed_integrity_hash) to handle pre-chain evidence:
treat a None integrity_hash or a None metadata_bundle or missing "token_id" as a
pre-rollout/backfilled row and skip blockchain tamper-failure by setting
blockchain_valid to True (or set an explicit pre_chain flag) instead of
comparing hashes; also avoid calling .get on None by using a safe default (e.g.,
metadata = evidence.metadata_bundle or {} and token_uuid =
metadata.get("token_id", "")) and only call ResolutionProofService._sign_payload
and compare when integrity_hash and required metadata are present so old rows
remain valid.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -19,7 +19,7 @@ | |||||||||||||||||||||
| GenerateRPTRequest, RPTResponse, | ||||||||||||||||||||||
| SubmitEvidenceRequest, EvidenceResponse, | ||||||||||||||||||||||
| VerificationResponse, AuditTrailResponse, | ||||||||||||||||||||||
| DuplicateCheckResponse, | ||||||||||||||||||||||
| DuplicateCheckResponse, BlockchainVerificationResponse, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||
|
|
@@ -201,6 +201,52 @@ def get_audit_log( | |||||||||||||||||||||
| # DUPLICATE / FRAUD DETECTION | ||||||||||||||||||||||
| # ============================================================================ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from backend.models import ResolutionEvidence | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @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).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 "" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Chaining logic: hash(evidence_hash|token_id|gps_latitude|gps_longitude|prev_hash) | ||||||||||||||||||||||
| # Re-derive token_id for hash (it's in metadata_bundle) | ||||||||||||||||||||||
| token_uuid = evidence.metadata_bundle.get("token_id", "") | ||||||||||||||||||||||
|
||||||||||||||||||||||
| token_uuid = evidence.metadata_bundle.get("token_id", "") | |
| metadata = evidence.metadata_bundle or {} | |
| token_uuid = metadata.get("token_id") | |
| if not token_uuid: | |
| raise HTTPException( | |
| status_code=422, | |
| detail="Evidence metadata is missing required field 'token_id' for blockchain verification.", | |
| ) |
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: Guard metadata_bundle before calling .get() to avoid 500 errors on records where the JSON column is null.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/resolution_proof.py, line 226:
<comment>Guard `metadata_bundle` before calling `.get()` to avoid 500 errors on records where the JSON column is null.</comment>
<file context>
@@ -201,6 +201,52 @@ def get_audit_log(
+
+ # Chaining logic: hash(evidence_hash|token_id|gps_latitude|gps_longitude|prev_hash)
+ # Re-derive token_id for hash (it's in metadata_bundle)
+ token_uuid = evidence.metadata_bundle.get("token_id", "")
+ hash_content = f"{evidence.evidence_hash}|{token_uuid}|{evidence.gps_latitude}|{evidence.gps_longitude}|{prev_hash}"
+ computed_hash = ResolutionProofService._sign_payload(hash_content)
</file context>
| token_uuid = evidence.metadata_bundle.get("token_id", "") | |
| token_uuid = (evidence.metadata_bundle or {}).get("token_id", "") |
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.
Guard nullable blockchain metadata in the public verifier.
backend/models.py::ResolutionEvidence.metadata_bundle is nullable, so older or partially migrated rows can 500 here at metadata_bundle.get(...). Normalize it to {} and return a deterministic invalid/unsupported result when the chain fields are absent instead of throwing from a public endpoint.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/routers/resolution_proof.py` around lines 221 - 228, The code assumes
evidence.metadata_bundle is a dict and calls metadata_bundle.get which can raise
when metadata_bundle is None; update the public verifier logic to normalize
metadata_bundle to {} (e.g., bundle = evidence.metadata_bundle or {}) before
accessing it, then extract token_uuid = bundle.get("token_id", "") and guard
required chain fields (token_id, gps_latitude, gps_longitude) on the evidence
object; if any are missing, return a deterministic invalid/unsupported response
(rather than proceeding to compute the chain hash), and only call
ResolutionProofService._sign_payload when all required fields are present and
valid.
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.
🧩 Analysis chain
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 121
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 554
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 3318
🏁 Script executed:
# Check the module docstring for Postgres references head -n 50 backend/init_db.pyRepository: RohanExploit/VishwaGuru
Length of output: 1505
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 360
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 229
🏁 Script executed:
# Check imports and setup to understand which databases are supported head -n 100 backend/init_db.pyRepository: RohanExploit/VishwaGuru
Length of output: 4475
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 1026
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 1096
🌐 Web query:
Postgres DATETIME type equivalent TIMESTAMP💡 Result:
In PostgreSQL, the equivalent to MySQL's DATETIME type is TIMESTAMP (specifically, TIMESTAMP WITHOUT TIME ZONE). Both store date and time without time zone information.
Citations:
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 406
DATETIMEmakes this migration Postgres-hostile.This module explicitly prevents transaction aborts in Postgres, but
ALTER TABLE ... ADD COLUMN ... DATETIMEis not valid Postgres DDL. Postgres has noDATETIMEtype; useTIMESTAMPorTIMESTAMP WITHOUT TIME ZONEinstead. Any Postgres deployment will fail on these three migration statements. Apply the same fix toexpires_at(line 225),valid_from(line 233), andvalid_until(line 237).🤖 Prompt for AI Agents