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
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@
## 2025-02-13 - API Route Prefix Consistency
**Learning:** Inconsistent application of `/api` prefixes between `main.py` router mounting and test suite request paths can lead to 404 errors during testing, even if the logic is correct. This is especially prevalent when multiple agents work on the same codebase with different assumptions about global prefixes.
**Action:** Always verify that `app.include_router` in `backend/main.py` uses `prefix="/api"` if the test suite (e.g., `tests/test_blockchain.py`) expects it. If a router is mounted without a prefix, ensure tests are updated or the prefix is added to `main.py` to maintain repository-wide consistency.

## 2026-02-15 - Implicit Model/Test Contracts
**Learning:** Discrepancies between model definitions and existing test suite expectations (e.g., `tests/test_resolution_proof.py` expecting `nonce` and `valid_from` columns on `ResolutionProofToken`) cause silent failures during optimization tasks.
**Action:** Before optimizing a service, verify that the underlying models fully satisfy the schemas and test assertions. Explicitly synchronize migrations and models when missing attributes are discovered during test execution.
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
27 changes: 27 additions & 0 deletions backend/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P2: Use TIMESTAMP instead of DATETIME for cross-database compatibility. The project supports both PostgreSQL (via DATABASE_URL) and SQLite. DATETIME is a SQLite-specific type; PostgreSQL requires TIMESTAMP. Other migrations in this file use portable types.

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

<comment>Use `TIMESTAMP` instead of `DATETIME` for cross-database compatibility. The project supports both PostgreSQL (via `DATABASE_URL`) and SQLite. `DATETIME` is a SQLite-specific type; PostgreSQL requires `TIMESTAMP`. Other migrations in this file use portable types.</comment>

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

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

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

Choose a reason for hiding this comment

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

P2: Same issue: use TIMESTAMP instead of DATETIME for PostgreSQL compatibility.

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

<comment>Same issue: use `TIMESTAMP` instead of `DATETIME` for PostgreSQL compatibility.</comment>

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

logger.info("Added valid_until column to resolution_proof_tokens")
Comment on lines +224 to +230
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 | 🟑 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what database type is configured in this project
rg -n 'DATABASE_URL|postgresql|sqlite' --type=py -C2

Repository: 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 -40

Repository: 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 -n

Repository: 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 -n

Repository: 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 -50

Repository: RohanExploit/VishwaGuru

Length of output: 2466


🏁 Script executed:

# Check database.py for any datetime handling
rg -n "DATETIME|TIMESTAMP|datetime|timestamp" backend/database.py

Repository: 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")
EOF

Repository: 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 -A2

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

Repository: RohanExploit/VishwaGuru

Length of output: 293


Change DATETIME to TIMESTAMP for PostgreSQL compatibility.

The code uses DATETIME which is SQLite-specific. PostgreSQL requires TIMESTAMP. Since the project supports both PostgreSQL (via DATABASE_URL) and SQLite, these columns should use TIMESTAMP to work across both databases. All other migrations in this file follow this cross-DB pattern using portable types (INTEGER, FLOAT, VARCHAR, TEXT).

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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", "valid_from"):
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 TIMESTAMP"))
logger.info("Added valid_until column to resolution_proof_tokens")
πŸ€– 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 - 230, The migration adds DATETIME
columns to resolution_proof_tokens which is SQLite-specific; update the ALTER
TABLE statements in init_db.py to use TIMESTAMP instead of DATETIME so they work
on PostgreSQL as well β€” locate the block using
column_exists("resolution_proof_tokens", "valid_from") and
column_exists("resolution_proof_tokens", "valid_until") and change the
conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_from
DATETIME")) and the corresponding valid_until statement to use TIMESTAMP; keep
the logger.info calls and behavior unchanged.


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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the current state of migrate_db() in main.py
rg -n 'migrate_db' backend/main.py -C3

Repository: RohanExploit/VishwaGuru

Length of output: 953


🏁 Script executed:

rg -n 'def submit_evidence' backend/ -A 20

Repository: RohanExploit/VishwaGuru

Length of output: 1540


🏁 Script executed:

rg -n 'integrity_hash|previous_integrity_hash|valid_from|valid_until' backend/ --type py

Repository: RohanExploit/VishwaGuru

Length of output: 10464


🏁 Script executed:

rg -n 'def submit_evidence' backend/resolution_proof_service.py -A 100

Repository: RohanExploit/VishwaGuru

Length of output: 4559


🏁 Script executed:

rg -n 'class ResolutionEvidence' backend/models.py -A 20

Repository: RohanExploit/VishwaGuru

Length of output: 1232


🏁 Script executed:

rg -n 'class ResolutionEvidence' backend/models.py -A 35

Repository: RohanExploit/VishwaGuru

Length of output: 2014


🏁 Script executed:

sed -n '385,410p' backend/resolution_proof_service.py

Repository: RohanExploit/VishwaGuru

Length of output: 984


🏁 Script executed:

grep -n "resolution_proof_tokens" backend/models.py | head -20

Repository: RohanExploit/VishwaGuru

Length of output: 208


🏁 Script executed:

sed -n '299,320p' backend/models.py

Repository: RohanExploit/VishwaGuru

Length of output: 1228


Re-enable migrations in main.py before deployment β€” submit_evidence requires new database columns.

The migrate_db() call is commented out at line 89 in main.py. The submit_evidence function in resolution_proof_service.py writes to integrity_hash and previous_integrity_hash on ResolutionEvidence (lines 397–398) and reads valid_from and valid_until from ResolutionProofToken (lines 331–332). If existing deployments have these tables without the new columns, database errors will occur when attempting to insert or read evidence records.

Either uncomment migrate_db() at line 89 or ensure the migration is run manually before deployment.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/init_db.py` around lines 209 - 234, Main issue: new migrations that
add integrity_hash and previous_integrity_hash to ResolutionEvidence and
valid_from/valid_until to ResolutionProofToken are required by submit_evidence
in resolution_proof_service.py (submit_evidence writes
ResolutionEvidence.integrity_hash and .previous_integrity_hash and reads
ResolutionProofToken.valid_from and .valid_until); fix by re-enabling the
database migration at startupβ€”uncomment the migrate_db() call in main.py (or
call migrate_db() before any code that may call submit_evidence) so the ALTER
TABLE/CREATE INDEX steps in init_db.py run before deployment, or alternatively
ensure migrations are executed manually prior to running the service.


Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The migration adds valid_from, valid_until, and nonce columns but doesn’t backfill them for existing resolution_proof_tokens rows. If any pre-migration tokens are still in use (or accessed during deployment), these NULLs can break token validation/signature checks. Consider updating existing rows after adding columns (e.g., set valid_from = generated_at and valid_until = expires_at, and generate a nonce if required for signature verification).

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

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

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

valid_from/valid_until are required by schemas and used unconditionally in token validation/signature payloads, but the model allows valid_until to be NULL. After migration, existing rows will have NULL in these new columns unless backfilled, which can trigger runtime errors when accessing .tzinfo or calling .isoformat(). Consider making these non-nullable (or adding a fallback/backfill strategy using generated_at/expires_at) to keep model invariants aligned with service expectations.

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

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

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

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>
Fix with Cubic

expires_at = Column(DateTime, nullable=False)
nonce = Column(String, nullable=True)
is_used = Column(Boolean, default=False)
used_at = Column(DateTime, nullable=True)
geofence_latitude = Column(Float, nullable=True)
Expand Down
35 changes: 29 additions & 6 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 @@ -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")
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P1: The cache stores a single global last_hash without any concurrency guard or DB-tail validation. In multi-worker or concurrent-request scenarios, two requests can read the same cached prev_hash, both commit with the same previous hash, and fork the chain. Additionally, the cache is only updated after commit, leaving a window where concurrent writers use stale data.

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
Check if this issue is valid β€” if so, understand the root cause and fix it. At backend/resolution_proof_service.py, line 374:

<comment>The cache stores a single global `last_hash` without any concurrency guard or DB-tail validation. In multi-worker or concurrent-request scenarios, two requests can read the same cached `prev_hash`, both commit with the same previous hash, and fork the chain. Additionally, the cache is only updated after commit, leaving a window where concurrent writers use stale data.

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.</comment>

<file context>
@@ -368,6 +369,19 @@ def submit_evidence(
 
+        # 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")
+        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_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
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

resolution_last_hash_cache stores a single global last_hash without validating it against the current DB tail. In multi-worker deployments or under concurrent submissions, two requests can read the same cached prev_hash and commit different rows, creating a forked/incorrect chain (and stale cache can chain to an old hash). Consider mirroring GrievanceService’s pattern: cache both last_id+last_hash and re-check the DB’s latest (id, integrity_hash) before using the cache; ideally compute prev_hash inside the same transaction as the insert to avoid races.

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_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",
)

Copilot uses AI. Check for mistakes.
# Chaining logic: HMAC-SHA256(evidence_hash|gps_lat|gps_lon|prev_hash)
hash_content = f"{evidence_hash}|{gps_latitude}|{gps_longitude}|{prev_hash}"
Copy link
Copy Markdown
Contributor

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

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>
Suggested change
hash_content = f"{evidence_hash}|{gps_latitude}|{gps_longitude}|{prev_hash}"
hash_content = f"{evidence_hash}|{gps_latitude:.8f}|{gps_longitude:.8f}|{prev_hash}"
Fix with Cubic

integrity_hash = ResolutionProofService._sign_payload(hash_content)
Comment on lines +381 to +383
Copy link

Copilot AI Apr 2, 2026

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 uses AI. Check for mistakes.

# 6. Create evidence record
evidence = ResolutionEvidence(
grievance_id=token.grievance_id,
Expand All @@ -380,6 +394,8 @@ def submit_evidence(
metadata_bundle=metadata_bundle,
server_signature=server_signature,
verification_status=VerificationStatus.VERIFIED,
integrity_hash=integrity_hash,
previous_integrity_hash=prev_hash
Comment on lines 381 to +398
Copy link

Copilot AI Apr 2, 2026

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.

Copilot uses AI. Check for mistakes.
)

db.add(evidence)
Expand All @@ -391,6 +407,9 @@ def submit_evidence(
db.commit()
db.refresh(evidence)

# Update cache for next evidence entry - ONLY after successful commit to prevent cache poisoning
resolution_last_hash_cache.set(data=integrity_hash, key="last_hash")

# 8. Create audit log
ResolutionProofService._create_audit_log(
evidence_id=evidence.id,
Expand Down Expand Up @@ -435,11 +454,13 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
Returns:
Verification result dictionary
"""
evidence_records = db.query(ResolutionEvidence).filter(
# Optimized: Use count() for total and fetch only the latest record
# This is O(1) in memory instead of loading all records with .all()
evidence_count = db.query(ResolutionEvidence).filter(
ResolutionEvidence.grievance_id == grievance_id
).all()
).count()

if not evidence_records:
if evidence_count == 0:
return {
"grievance_id": grievance_id,
"is_verified": False,
Expand All @@ -452,8 +473,10 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
"message": "No resolution evidence found for this grievance"
}

# Use the most recent evidence
evidence = evidence_records[-1]
# Fetch ONLY the most recent evidence record
evidence = db.query(ResolutionEvidence).filter(
ResolutionEvidence.grievance_id == grievance_id
).order_by(ResolutionEvidence.id.desc()).first()

# Re-verify the server signature
bundle_str = json.dumps(evidence.metadata_bundle, sort_keys=True)
Expand Down Expand Up @@ -494,7 +517,7 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
"location_match": location_match,
"evidence_integrity": signature_valid,
"evidence_hash": evidence.evidence_hash,
"evidence_count": len(evidence_records),
"evidence_count": evidence_count,
"message": (
"Resolution verified with cryptographic proof"
if is_verified
Expand Down
60 changes: 60 additions & 0 deletions backend/routers/resolution_proof.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
SubmitEvidenceRequest, EvidenceResponse,
VerificationResponse, AuditTrailResponse,
DuplicateCheckResponse,
BlockchainVerificationResponse
)

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

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

No tests appear to cover the new resolution-evidence blockchain seal and the /{evidence_id}/blockchain-verify endpoint (e.g., tests/test_resolution_proof.py has no references to integrity_hash or blockchain-verify). Adding a couple of tests (happy path + tamper case) would help prevent regressions in the chaining payload/canonicalization and cache/prev-hash logic.

Copilot uses AI. Check for mistakes.

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

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

Choose a reason for hiding this comment

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

P2: Use hmac.compare_digest() instead of == for HMAC comparison to prevent timing side-channel attacks. The codebase already uses hmac.compare_digest in _verify_signature β€” this comparison should be consistent.

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

<comment>Use `hmac.compare_digest()` instead of `==` for HMAC comparison to prevent timing side-channel attacks. The codebase already uses `hmac.compare_digest` in `_verify_signature` β€” this comparison should be consistent.</comment>

<file context>
@@ -217,3 +218,62 @@ def flag_duplicate_evidence(
+            is_valid = False
+            message = "No integrity hash present; cryptographic integrity cannot be verified."
+        else:
+            is_valid = (computed_hash == evidence.integrity_hash)
+            message = (
+                "Integrity verified. This evidence record is cryptographically sealed."
</file context>
Fix with Cubic

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

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

computed_hash == evidence.integrity_hash is used for comparison. Since this is an HMAC-based value, prefer hmac.compare_digest (as already done in _verify_signature) to avoid timing side-channels and keep comparisons consistent across the service.

Copilot uses AI. Check for mistakes.
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")
Loading