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
37 changes: 37 additions & 0 deletions backend/init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,43 @@ 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 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_grievance_id"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_resolution_evidence_grievance_id ON resolution_evidence (grievance_id)"))

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 Migrations
if inspector.has_table("resolution_proof_tokens"):
if not column_exists("resolution_proof_tokens", "token_id"):
conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN token_id VARCHAR"))
logger.info("Added token_id 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"):
conn.execute(text("ALTER TABLE resolution_proof_tokens ADD COLUMN valid_until DATETIME"))
Comment on lines +236 to +240
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

The migration uses DATETIME in raw SQL (ALTER TABLE ... ADD COLUMN valid_from DATETIME), which is not a valid type in PostgreSQL (Render can run with DATABASE_URL). Use TIMESTAMP/TIMESTAMPTZ (or a SQLAlchemy-compiled type) to keep migrations portable across SQLite/Postgres.

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

Copilot uses AI. Check for mistakes.
logger.info("Added valid_until column to resolution_proof_tokens")

if not index_exists("resolution_proof_tokens", "ix_resolution_proof_tokens_grievance_id"):
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_resolution_proof_tokens_grievance_id ON resolution_proof_tokens (grievance_id)"))

logger.info("Database migration check completed successfully.")

except Exception as e:
Expand Down
13 changes: 13 additions & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ class VerificationStatus(enum.Enum):

class ResolutionEvidence(Base):
__tablename__ = "resolution_evidence"
__table_args__ = (
Index("ix_resolution_evidence_grievance_id", "grievance_id"),
)
id = Column(Integer, primary_key=True, index=True)
grievance_id = Column(Integer, ForeignKey("grievances.id"), nullable=False)
token_id = Column(Integer, ForeignKey("resolution_proof_tokens.id"), nullable=True)
Expand All @@ -286,6 +289,9 @@ class ResolutionEvidence(Base):
metadata_bundle = Column(JSON, nullable=True)
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)

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

ResolutionEvidence still has no created_at field (it only defines uploaded_at earlier in the model), but the resolution proof router/service and EvidenceResponse schema reference created_at (e.g., created_at=evidence.created_at). This will raise AttributeError at runtime unless there’s an alias/property elsewhere. Consider adding a created_at column (or aliasing uploaded_at to created_at) and migrating/backfilling as needed so the ORM model matches the API/schema expectations.

Suggested change
@property
def created_at(self) -> datetime.datetime | None:
"""
Alias for uploaded_at to satisfy API/schema expectations.
This avoids changing the DB schema while exposing created_at.
"""
return self.uploaded_at
@created_at.setter
def created_at(self, value: datetime.datetime | None) -> None:
self.uploaded_at = value

Copilot uses AI. Check for mistakes.
# Relationships
grievance = relationship("Grievance", back_populates="resolution_evidence")
Expand All @@ -294,13 +300,20 @@ class ResolutionEvidence(Base):

class ResolutionProofToken(Base):
__tablename__ = "resolution_proof_tokens"
__table_args__ = (
Index("ix_resolution_proof_tokens_grievance_id", "grievance_id"),
)
id = Column(Integer, primary_key=True, index=True)
grievance_id = Column(Integer, ForeignKey("grievances.id"), nullable=False)
token = Column(String, unique=True, index=True, nullable=True)
token_id = Column(String, unique=True, index=True, nullable=True) # UUID string
authority_email = Column(String, nullable=True)
generated_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc))
expires_at = Column(DateTime, nullable=False)
# Validity fields (Issue #292)
nonce = Column(String, nullable=True)
valid_from = Column(DateTime, nullable=True)
valid_until = Column(DateTime, nullable=True)
is_used = Column(Boolean, default=False)
used_at = Column(DateTime, nullable=True)
geofence_latitude = Column(Float, nullable=True)
Expand Down
120 changes: 103 additions & 17 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 @@ -172,6 +173,10 @@ def generate_proof_token(
now = datetime.now(timezone.utc)
valid_until = now + timedelta(minutes=TOKEN_VALIDITY_MINUTES)

# Use fixed format for deterministic hashing
valid_from_str = now.strftime('%Y-%m-%dT%H:%M:%S')
valid_until_str = valid_until.strftime('%Y-%m-%dT%H:%M:%S')

# Build signing payload
payload = json.dumps({
"token_id": token_uuid,
Expand All @@ -180,15 +185,17 @@ def generate_proof_token(
"geofence_lat": grievance.latitude,
"geofence_lon": grievance.longitude,
"geofence_radius": geofence_radius,
"valid_from": now.isoformat(),
"valid_until": valid_until.isoformat(),
"valid_from": valid_from_str,
"valid_until": valid_until_str,
"nonce": nonce
}, sort_keys=True)

signature = ResolutionProofService._sign_payload(payload)

# Create token record
# Populate both 'token' and 'token_id' for compatibility and to fix AttributeErrors
token = ResolutionProofToken(
token=token_uuid,
token_id=token_uuid,
grievance_id=grievance_id,
authority_email=authority_email,
Expand All @@ -197,6 +204,7 @@ def generate_proof_token(
geofence_radius_meters=geofence_radius,
valid_from=now,
valid_until=valid_until,
expires_at=valid_until, # Required field
nonce=nonce,
token_signature=signature,
is_used=False,
Expand Down Expand Up @@ -234,8 +242,9 @@ def validate_token(token_id: str, db: Session) -> ResolutionProofToken:
Raises:
ValueError: If any validation check fails
"""
# Fix: Query against 'token' column as it's the primary/unique identifier in some versions
token = db.query(ResolutionProofToken).filter(
ResolutionProofToken.token_id == token_id
ResolutionProofToken.token == token_id
).first()
Comment on lines +245 to 248
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

validate_token() now queries only ResolutionProofToken.token == token_id, but the API and existing data model also uses token_id (and the router returns token.token_id). If older rows have token NULL (or clients submit token_id while token differs), token validation will incorrectly fail. Consider querying with an OR on both columns (and/or backfilling token in the migration) to preserve compatibility.

Copilot uses AI. Check for mistakes.

if not token:
Expand All @@ -261,15 +270,19 @@ def validate_token(token_id: str, db: Session) -> ResolutionProofToken:
if valid_from.tzinfo is None:
valid_from = valid_from.replace(tzinfo=timezone.utc)

# Use fixed format for deterministic hashing
valid_from_str = valid_from.strftime('%Y-%m-%dT%H:%M:%S')
valid_until_str = valid_until.strftime('%Y-%m-%dT%H:%M:%S')

payload = json.dumps({
"token_id": token.token_id,
"grievance_id": token.grievance_id,
"authority_email": token.authority_email,
"geofence_lat": token.geofence_latitude,
"geofence_lon": token.geofence_longitude,
"geofence_radius": token.geofence_radius_meters,
"valid_from": valid_from.isoformat(),
"valid_until": valid_until.isoformat(),
"valid_from": valid_from_str,
"valid_until": valid_until_str,
"nonce": token.nonce
}, sort_keys=True)

Expand Down Expand Up @@ -352,23 +365,40 @@ def submit_evidence(
f"for grievance(s): {dup_ids}. Possible fraud."
)

# 5. Create server-side signed metadata bundle
# 5. Blockchain chaining logic
# 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 Mar 31, 2026

Choose a reason for hiding this comment

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

P1: The blockchain chaining logic has a race condition under concurrent submissions. Two simultaneous submit_evidence calls can both read the same prev_hash from the cache, compute independent integrity_hash values, and commit — creating a fork where two evidence records share the same previous_integrity_hash. This breaks the append-only chain invariant that the feature is designed to guarantee.

Consider serializing chain appends with a database-level lock (e.g., SELECT ... FOR UPDATE on a chain-tip row, or a DB advisory lock) so that only one submission at a time can read and extend the chain.

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

<comment>The blockchain chaining logic has a race condition under concurrent submissions. Two simultaneous `submit_evidence` calls can both read the same `prev_hash` from the cache, compute independent `integrity_hash` values, and commit — creating a fork where two evidence records share the same `previous_integrity_hash`. This breaks the append-only chain invariant that the feature is designed to guarantee.

Consider serializing chain appends with a database-level lock (e.g., `SELECT ... FOR UPDATE` on a chain-tip row, or a DB advisory lock) so that only one submission at a time can read and extend the chain.</comment>

<file context>
@@ -352,23 +365,40 @@ def submit_evidence(
-        # 5. Create server-side signed metadata bundle
+        # 5. Blockchain chaining logic
+        # 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
Comment on lines +368 to +372
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

The chain-tip cache approach here can break linear chaining under concurrency and in multi-worker/multi-instance deployments: two submissions can read the same cached/DB tip and both commit with the same previous_integrity_hash (fork), and separate processes will have divergent in-memory caches (stale prev_hash). To keep the chain append-only and deterministic, the previous hash needs to be read/updated atomically at the DB level (e.g., transaction + row/advisory lock or a dedicated single-row “chain tip” table with optimistic locking); use cache only as an optimization after verifying the DB tip hasn’t changed.

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

# Chaining: hash(token_id|evidence_hash|gps|ts|prev_hash)
# Use fixed timestamp format for deterministic hashing
cap_ts_str = cap_ts.strftime('%Y-%m-%dT%H:%M:%S')
chain_content = f"{token.token_id}|{evidence_hash}|{gps_latitude}|{gps_longitude}|{cap_ts_str}|{prev_hash}"
integrity_hash = hashlib.sha256(chain_content.encode()).hexdigest()

# 6. Create server-side signed metadata bundle
metadata_bundle = {
"token_id": token.token_id,
"grievance_id": token.grievance_id,
"authority_email": token.authority_email,
"evidence_hash": evidence_hash,
"gps_latitude": gps_latitude,
"gps_longitude": gps_longitude,
"capture_timestamp": cap_ts.isoformat(),
"capture_timestamp": cap_ts_str,
"device_fingerprint_hash": device_fingerprint_hash,
"geofence_distance_meters": distance,
"integrity_hash": integrity_hash,
"previous_integrity_hash": prev_hash
}

bundle_str = json.dumps(metadata_bundle, sort_keys=True)
server_signature = ResolutionProofService._sign_payload(bundle_str)

# 6. Create evidence record
# 7. Create evidence record
evidence = ResolutionEvidence(
grievance_id=token.grievance_id,
token_id=token.id,
Expand All @@ -380,18 +410,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 for next submission AFTER successful commit
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 @@ -422,6 +457,53 @@ def submit_evidence(
# Verification
# ──────────────────────────────────────────────

@staticmethod
def verify_evidence_integrity(evidence_id: int, db: Session) -> Dict[str, Any]:
"""
Verify the cryptographic integrity of a single resolution evidence record.
Optimized: Uses previous_integrity_hash column for O(1) verification.
"""
evidence = db.query(ResolutionEvidence).filter(ResolutionEvidence.id == evidence_id).first()
if not evidence:
raise ValueError(f"Evidence {evidence_id} not found")

# Determine previous hash (O(1) from stored column)
prev_hash = evidence.previous_integrity_hash or ""

# Re-verify the server signature
bundle_str = json.dumps(evidence.metadata_bundle, sort_keys=True)
signature_valid = ResolutionProofService._verify_signature(
bundle_str, evidence.server_signature
)

# Recompute chaining hash
cap_ts = evidence.capture_timestamp
if cap_ts.tzinfo is None:
cap_ts = cap_ts.replace(tzinfo=timezone.utc)
cap_ts_str = cap_ts.strftime('%Y-%m-%dT%H:%M:%S')

# Fix: Query token explicitly using the ID stored in evidence
token = db.query(ResolutionProofToken.token_id).filter(ResolutionProofToken.id == evidence.token_id).first()
token_id_val = token[0] if token else ""

chain_content = f"{token_id_val}|{evidence.evidence_hash}|{evidence.gps_latitude}|{evidence.gps_longitude}|{cap_ts_str}|{prev_hash}"
computed_hash = hashlib.sha256(chain_content.encode()).hexdigest()

is_valid = (computed_hash == evidence.integrity_hash) and signature_valid

if is_valid:
message = "Integrity verified. This resolution evidence is cryptographically sealed and part of a secure chain."
else:
message = "Integrity check failed! The evidence data does not match its cryptographic seal."

return {
"is_valid": is_valid,
"current_hash": evidence.integrity_hash,
"computed_hash": computed_hash,
"signature_valid": signature_valid,
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

verify_evidence_integrity() includes signature_valid in the returned dict, but ResolutionBlockchainVerificationResponse doesn’t define this field. This is either silently dropped (losing useful information) or can raise validation errors depending on Pydantic config. Either add signature_valid: bool to the response schema or stop returning it here and keep the result strictly schema-shaped.

Suggested change
"signature_valid": signature_valid,

Copilot uses AI. Check for mistakes.
"message": message
}

@staticmethod
def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
"""
Expand All @@ -435,11 +517,13 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:
Returns:
Verification result dictionary
"""
evidence_records = db.query(ResolutionEvidence).filter(
# Optimized: Fetch only the latest evidence and the count in separate optimized queries
# This is more efficient than loading ALL evidence records into memory
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,8 +536,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]
# Get total count efficiently
evidence_count = db.query(ResolutionEvidence).filter(
ResolutionEvidence.grievance_id == grievance_id
).count()

# Re-verify the server signature
bundle_str = json.dumps(evidence.metadata_bundle, sort_keys=True)
Expand Down Expand Up @@ -483,7 +569,7 @@ def verify_evidence(grievance_id: int, db: Session) -> Dict[str, Any]:

status_str = evidence.verification_status.value if evidence.verification_status else "pending"

grievance = db.query(Grievance).filter(Grievance.id == grievance_id).first()
grievance = db.query(Grievance.resolved_at).filter(Grievance.id == grievance_id).first()
resolution_ts = grievance.resolved_at if grievance else None

return {
Expand All @@ -494,7 +580,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
21 changes: 20 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, ResolutionBlockchainVerificationResponse
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -145,6 +145,25 @@ def verify_resolution(
raise HTTPException(status_code=500, detail="Failed to verify resolution")


@router.get("/{evidence_id}/blockchain-verify", response_model=ResolutionBlockchainVerificationResponse)
def verify_evidence_blockchain(
evidence_id: int,
db: Session = Depends(get_db)
):
Comment on lines +148 to +152
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

Given backend/main.py mounts this router with prefix="/api", and this router already uses prefix="/api/resolution-proof", this new endpoint will be exposed at /api/api/resolution-proof/{evidence_id}/blockchain-verify. If the intended public path is /api/resolution-proof/... (consistent with other routers that use APIRouter() without an /api prefix), consider removing /api from this router’s prefix or adjusting the include_router prefix.

Copilot uses AI. Check for mistakes.
"""
Verify the cryptographic integrity of a single resolution evidence record using blockchain-style chaining.
Optimized: Uses previous_integrity_hash column for O(1) verification.
"""
try:
result = ResolutionProofService.verify_evidence_integrity(evidence_id, db)
return ResolutionBlockchainVerificationResponse(**result)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
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 resolution integrity")


# ============================================================================
# EVIDENCE RETRIEVAL
# ============================================================================
Expand Down
9 changes: 9 additions & 0 deletions backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,19 @@ class EvidenceResponse(BaseModel):
capture_timestamp: datetime = Field(..., description="When evidence was captured")
verification_status: str = Field(..., description="Verification status: pending, verified, flagged, fraud_detected")
server_signature: str = Field(..., description="Server cryptographic signature")
integrity_hash: Optional[str] = Field(None, description="Blockchain integrity hash")
previous_integrity_hash: Optional[str] = Field(None, description="Previous blockchain hash")
created_at: datetime = Field(..., description="Record creation timestamp")
message: str = Field(..., description="Status message")


class ResolutionBlockchainVerificationResponse(BaseModel):
is_valid: bool = Field(..., description="Whether the resolution integrity is intact")
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

ResolutionBlockchainVerificationResponse omits signature_valid, even though the service returns it and it’s relevant to integrity verification. Consider adding signature_valid: bool so clients can distinguish signature failures from chaining-hash failures (or remove it from the service result to keep the contract minimal).

Suggested change
is_valid: bool = Field(..., description="Whether the resolution integrity is intact")
is_valid: bool = Field(..., description="Whether the resolution integrity is intact")
signature_valid: bool = Field(..., description="Whether the cryptographic signature verification succeeded")

Copilot uses AI. Check for mistakes.
current_hash: Optional[str] = Field(None, description="Current integrity hash stored in DB")
computed_hash: str = Field(..., description="Hash computed from current evidence data and previous record's hash")
message: str = Field(..., description="Verification result message")


class VerificationResponse(BaseModel):
grievance_id: int = Field(..., description="Grievance ID")
is_verified: bool = Field(..., description="Whether the resolution is cryptographically verified")
Expand Down
Loading
Loading