Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 31 additions & 0 deletions backend/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Comment on lines +224 to +225
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the relevant files
find backend -type f -name "*.py" | grep -E "(init_db|resolution_proof_service)" | head -20

Repository: RohanExploit/VishwaGuru

Length of output: 121


🏁 Script executed:

# Get the repository structure
git ls-files backend | head -20

Repository: RohanExploit/VishwaGuru

Length of output: 554


🏁 Script executed:

# Read the relevant section of init_db.py
head -n 250 backend/init_db.py | tail -n 50

Repository: RohanExploit/VishwaGuru

Length of output: 3318


🏁 Script executed:

# Check the module docstring for Postgres references
head -n 50 backend/init_db.py

Repository: RohanExploit/VishwaGuru

Length of output: 1505


🏁 Script executed:

# Check the validation code in resolution_proof_service.py
grep -A 10 "def validate_token" backend/resolution_proof_service.py

Repository: RohanExploit/VishwaGuru

Length of output: 360


🏁 Script executed:

# Check if there's any database dialect handling in the codebase
rg -i "postgres|sqlite|mysql|dialect" backend/init_db.py -A 2 -B 2

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.py

Repository: RohanExploit/VishwaGuru

Length of output: 4475


🏁 Script executed:

# Read more of the resolution_proof_service.py to see how these columns are used
grep -A 30 "def validate_token" backend/resolution_proof_service.py

Repository: RohanExploit/VishwaGuru

Length of output: 1026


🏁 Script executed:

# Check database configuration
cat backend/database.py | head -50

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:

# Verify there are exactly 3 DATETIME instances in the migration section
grep -n "DATETIME" backend/init_db.py

Repository: RohanExploit/VishwaGuru

Length of output: 406


DATETIME makes this migration Postgres-hostile.

This module explicitly prevents transaction aborts in Postgres, but ALTER TABLE ... ADD COLUMN ... DATETIME is not valid Postgres DDL. Postgres has no DATETIME type; use TIMESTAMP or TIMESTAMP WITHOUT TIME ZONE instead. Any Postgres deployment will fail on these three migration statements. Apply the same fix to expires_at (line 225), valid_from (line 233), and valid_until (line 237).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/init_db.py` around lines 224 - 225, The migration uses the
non-Postgres type DATETIME in ALTER TABLE statements (e.g. the block that checks
column_exists("resolution_proof_tokens", "expires_at") and then calls
conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN expires_at
DATETIME"))), which will fail on Postgres; update those ALTER TABLE statements
to use a Postgres-compatible type such as TIMESTAMP or TIMESTAMP WITHOUT TIME
ZONE for the expires_at, valid_from, and valid_until columns (the same change
applies to the other similar blocks creating valid_from and valid_until),
keeping the existing column_exists checks and conn.execute(text(...)) calls but
replacing DATETIME with TIMESTAMP (or TIMESTAMP WITHOUT TIME ZONE).

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"):
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Migration adds nonce, valid_from, and valid_until columns but doesn't backfill existing resolution_proof_tokens rows. Since the service code sets and reads these fields (and assumes non-NULL values for signing/validation), pre-existing tokens will have NULLs and can cause runtime errors. Backfill from existing columns (e.g., valid_from=generated_at, valid_until=expires_at) and set a default nonce for legacy rows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/init_db.py, line 236:

<comment>Migration adds `nonce`, `valid_from`, and `valid_until` columns but doesn't backfill existing `resolution_proof_tokens` rows. Since the service code sets and reads these fields (and assumes non-NULL values for signing/validation), pre-existing tokens will have NULLs and can cause runtime errors. Backfill from existing columns (e.g., `valid_from=generated_at`, `valid_until=expires_at`) and set a default nonce for legacy rows.</comment>

<file context>
@@ -206,6 +206,37 @@ def index_exists(table, index_name):
+                    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")
</file context>
Fix with Cubic

conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_until DATETIME"))
Comment on lines +228 to +237
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Backfill the new token metadata or keep validation backward compatible.

backend/resolution_proof_service.py::ResolutionProofService.validate_token now depends on valid_from / valid_until and the new token metadata when rebuilding the signature payload. Any still-valid resolution_proof_tokens row created before this migration will keep NULL here, so validation can start failing immediately after deploy. Populate the new fields from existing data where possible, or keep a legacy-validation fallback until those rows age out.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/init_db.py` around lines 228 - 237, Migration adds nonce, valid_from,
valid_until columns to resolution_proof_tokens but existing rows will have NULLs
and ResolutionProofService.validate_token rebuilds signature payload using these
fields, causing validation failures; either backfill the new columns in
init_db.py (populate nonce/valid_from/valid_until from existing token fields or
sensible defaults derived from created_at/expiry columns) or update
ResolutionProofService.validate_token to detect NULL
nonce/valid_from/valid_until and use the legacy payload/validation flow as a
fallback until all rows are backfilled; ensure you reference the
resolution_proof_tokens table and the fields nonce, valid_from, valid_until and
update init_db.py migration block or ResolutionProofService.validate_token
accordingly so validation remains backward compatible.

logger.info("Added valid_until column to resolution_proof_tokens")

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migration adds nonce, valid_from, and valid_until columns but doesn’t backfill existing resolution_proof_tokens rows. Since the service uses these fields (and assumes they’re non-NULL), pre-existing tokens can cause 500s when validated/submitting evidence. Consider backfilling from existing columns (e.g., valid_from=generated_at, valid_until=expires_at) and setting a nonce or marking legacy tokens as used/invalid.

Suggested change
# 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
"""
)
)

Copilot uses AI. Check for mistakes.
logger.info("Database migration check completed successfully.")

except Exception as e:
Expand Down
7 changes: 7 additions & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResolutionProofToken.nonce, valid_from, and valid_until are introduced as nullable, but token validation/submission logic calls .tzinfo/.isoformat() on these fields and includes nonce in the signed payload. Existing rows will get NULLs after the ALTER TABLE migration, which can cause runtime errors. Either make these columns non-nullable with a migration backfill (e.g., valid_from=generated_at, valid_until=expires_at, nonce=) or update validation to handle NULLs by rejecting such tokens cleanly.

Copilot uses AI. Check for mistakes.

# Relationship
grievance = relationship("Grievance", back_populates="resolution_tokens")
Expand Down
49 changes: 39 additions & 10 deletions backend/resolution_proof_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Concurrent submit_evidence calls will both read the same prev_hash from the in-memory cache, causing two records to chain from the same predecessor and permanently forking the integrity chain. The ThreadSafeCache lock only guards dict access, not the read → compute → commit → update-cache cycle.

To make this correct, the previous hash must be obtained under a serializable guarantee — e.g., a database-level SELECT … FOR UPDATE on the last evidence row (or a DB advisory lock) so that concurrent writers are sequenced. The in-memory cache can still sit in front as a fast-path, but the DB lock must be the source of truth for ordering.

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 375:

<comment>Concurrent `submit_evidence` calls will both read the same `prev_hash` from the in-memory cache, causing two records to chain from the same predecessor and permanently forking the integrity chain. The `ThreadSafeCache` lock only guards dict access, not the read → compute → commit → update-cache cycle.

To make this correct, the previous hash must be obtained under a serializable guarantee — e.g., a database-level `SELECT … FOR UPDATE` on the last evidence row (or a DB advisory lock) so that concurrent writers are sequenced. The in-memory cache can still sit in front as a fast-path, but the DB lock must be the source of truth for ordering.</comment>

<file context>
@@ -368,7 +370,20 @@ def submit_evidence(
-        # 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")
+        if prev_hash is None:
+            # Cache miss: Fetch only the last hash from DB
</file context>
Fix with Cubic

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
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The integrity-chain cache is used without validating that it still matches the current DB tail. In concurrent submissions or multi-worker deployments, multiple requests can read the same cached "last_hash" and create forks (non-linear chain) or chain to a stale hash. Consider caching both (last_id,last_hash) and refreshing from DB when the cache doesn’t match the latest row (similar to GrievanceService’s cache validation), or otherwise serialize/lock the tail read+insert so the chain stays linear.

Suggested change
# 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 uses AI. Check for mistakes.
# Chaining logic: hash(evidence_hash|token_id|gps_latitude|gps_longitude|prev_hash)
hash_content = f"{evidence_hash}|{token.token_id}|{gps_latitude}|{gps_longitude}|{prev_hash}"
integrity_hash = ResolutionProofService._sign_payload(hash_content)

# 7. Create evidence record
evidence = ResolutionEvidence(
grievance_id=token.grievance_id,
token_id=token.id,
Expand All @@ -380,18 +395,23 @@ 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)

# 7. Mark token as used
# 8. Mark token as used
token.is_used = True
token.used_at = datetime.now(timezone.utc)

db.commit()
db.refresh(evidence)

# 8. Create audit log
# Update cache after successful commit to prevent cache poisoning
resolution_last_hash_cache.set(data=integrity_hash, key="last_hash")

# 9. Create audit log
ResolutionProofService._create_audit_log(
evidence_id=evidence.id,
action="created",
Expand Down Expand Up @@ -430,16 +450,18 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
Checks:
- Evidence exists
- Evidence hash integrity (re-sign and compare)
- Blockchain integrity (recompute chain hash)
- Location match (within geofence)

Returns:
Verification result dictionary
"""
evidence_records = db.query(ResolutionEvidence).filter(
# Performance Boost: Fetch only the latest record to minimize DB load
evidence = db.query(ResolutionEvidence).filter(
ResolutionEvidence.grievance_id == grievance_id
).all()
).order_by(ResolutionEvidence.id.desc()).first()

if not evidence_records:
if not evidence:
return {
"grievance_id": grievance_id,
"is_verified": False,
Expand All @@ -452,9 +474,6 @@ 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]

# Re-verify the server signature
bundle_str = json.dumps(evidence.metadata_bundle, sort_keys=True)
signature_valid = ResolutionProofService._verify_signature(
Expand All @@ -475,9 +494,18 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
)
location_match = is_inside

# 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", "")
Copy link

Copilot AI Apr 1, 2026

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).

Suggested change
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 = ""

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 1, 2026

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>
Suggested change
token_uuid = evidence.metadata_bundle.get("token_id", "")
metadata = evidence.metadata_bundle or {}
token_uuid = metadata.get("token_id", "")
Fix with Cubic

hash_content = f"{evidence.evidence_hash}|{token_uuid}|{evidence.gps_latitude}|{evidence.gps_longitude}|{prev_hash}"
computed_integrity_hash = ResolutionProofService._sign_payload(hash_content)
blockchain_valid = (computed_integrity_hash == evidence.integrity_hash)
Comment on lines +497 to +503
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.


is_verified = (
signature_valid and
location_match and
blockchain_valid and
evidence.verification_status == VerificationStatus.VERIFIED
)

Expand All @@ -493,8 +521,9 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
"resolution_timestamp": resolution_ts,
"location_match": location_match,
"evidence_integrity": signature_valid,
"blockchain_integrity": blockchain_valid,
"evidence_hash": evidence.evidence_hash,
"evidence_count": len(evidence_records),
"evidence_count": db.query(ResolutionEvidence).filter(ResolutionEvidence.grievance_id == grievance_id).count(),
"message": (
"Resolution verified with cryptographic proof"
if is_verified
Expand Down
48 changes: 47 additions & 1 deletion backend/routers/resolution_proof.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
GenerateRPTRequest, RPTResponse,
SubmitEvidenceRequest, EvidenceResponse,
VerificationResponse, AuditTrailResponse,
DuplicateCheckResponse,
DuplicateCheckResponse, BlockchainVerificationResponse,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -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", "")
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint assumes evidence.metadata_bundle is a dict and calls .get("token_id"), but the column is nullable and legacy evidence rows may have NULL metadata. That will raise an AttributeError and return a 500. Use metadata = evidence.metadata_bundle or {} (and return a clear 400/422 or is_valid=false when required fields are missing).

Suggested change
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.",
)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 1, 2026

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>
Suggested change
token_uuid = evidence.metadata_bundle.get("token_id", "")
token_uuid = (evidence.metadata_bundle or {}).get("token_id", "")
Fix with Cubic

hash_content = f"{evidence.evidence_hash}|{token_uuid}|{evidence.gps_latitude}|{evidence.gps_longitude}|{prev_hash}"
computed_hash = ResolutionProofService._sign_payload(hash_content)
Comment on lines +221 to +228
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.


is_valid = (computed_hash == evidence.integrity_hash)

if is_valid:
message = "Integrity verified. This evidence record is cryptographically sealed and part of a secure chain."
else:
message = "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")


@router.post("/flag-duplicate", response_model=DuplicateCheckResponse)
def flag_duplicate_evidence(
evidence_hash: str,
Expand Down
4 changes: 2 additions & 2 deletions backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,9 @@ class ClosureStatusResponse(BaseModel):
days_remaining: Optional[int] = Field(None, description="Days until deadline")

class BlockchainVerificationResponse(BaseModel):
is_valid: bool = Field(..., description="Whether the issue integrity is intact")
is_valid: bool = Field(..., description="Whether the issue/record integrity is intact")
current_hash: Optional[str] = Field(None, description="Current integrity hash stored in DB")
computed_hash: str = Field(..., description="Hash computed from current issue data and previous issue's hash")
computed_hash: str = Field(..., description="Hash computed from current data and previous hash")
message: str = Field(..., description="Verification result message")


Expand Down
Loading
Loading