-
Notifications
You must be signed in to change notification settings - Fork 37
⚡ Bolt: Optimized Blockchain Integrity Seal & Issue Retrieval #410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9b6b686
9099413
eb4d0b9
4e411ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,7 @@ | |
| from backend.models import Issue, PushSubscription | ||
| from backend.schemas import ( | ||
| IssueCreateWithDeduplicationResponse, IssueCategory, NearbyIssueResponse, | ||
| DeduplicationCheckResponse, IssueSummaryResponse, VoteResponse, | ||
| DeduplicationCheckResponse, IssueSummaryResponse, IssueResponse, VoteResponse, | ||
| IssueStatusUpdateRequest, IssueStatusUpdateResponse, PushSubscriptionRequest, | ||
| PushSubscriptionResponse, BlockchainVerificationResponse | ||
| ) | ||
|
|
@@ -168,15 +168,22 @@ async def create_issue( | |
| try: | ||
| # Save to DB only if no nearby issues found or deduplication failed | ||
| if deduplication_info is None or not deduplication_info.has_nearby_issues: | ||
| # Blockchain feature: calculate integrity hash for the report | ||
| # Optimization: Fetch only the last hash to maintain the chain with minimal overhead | ||
| # Robust Blockchain Implementation | ||
| # 1. Fetch only the last hash to maintain the chain with minimal overhead | ||
| prev_issue = await run_in_threadpool( | ||
| lambda: db.query(Issue.integrity_hash).order_by(Issue.id.desc()).first() | ||
| ) | ||
|
Comment on lines
+171
to
175
|
||
| prev_hash = prev_issue[0] if prev_issue and prev_issue[0] else "" | ||
|
Comment on lines
173
to
176
|
||
|
|
||
| # Simple but effective SHA-256 chaining | ||
| hash_content = f"{description}|{category}|{prev_hash}" | ||
| # 2. Generate secure reference ID | ||
| reference_id = str(uuid.uuid4()) | ||
|
|
||
| # 3. Calculate robust integrity hash incorporating multiple fields | ||
| # Chaining logic: hash(ref_id|desc|cat|lat|lon|email|prev_hash) | ||
| # Use fixed float formatting to ensure consistent hashing across environments | ||
| lat_str = f"{latitude:.7f}" if latitude is not None else "None" | ||
| lon_str = f"{longitude:.7f}" if longitude is not None else "None" | ||
| hash_content = f"{reference_id}|{description}|{category}|{lat_str}|{lon_str}|{user_email}|{prev_hash}" | ||
| integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest() | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # RAG Retrieval (New) | ||
|
|
@@ -186,7 +193,7 @@ async def create_issue( | |
| initial_action_plan = {"relevant_government_rule": relevant_rule} | ||
|
|
||
| new_issue = Issue( | ||
| reference_id=str(uuid.uuid4()), | ||
| reference_id=reference_id, | ||
| description=description, | ||
| category=category, | ||
| image_path=image_path, | ||
|
|
@@ -196,7 +203,8 @@ async def create_issue( | |
| longitude=longitude, | ||
| location=location, | ||
| action_plan=initial_action_plan, | ||
| integrity_hash=integrity_hash | ||
| integrity_hash=integrity_hash, | ||
| previous_integrity_hash=prev_hash | ||
| ) | ||
|
|
||
| # Offload blocking DB operations to threadpool | ||
|
|
@@ -615,30 +623,44 @@ def get_user_issues( | |
| async def verify_blockchain_integrity(issue_id: int, db: Session = Depends(get_db)): | ||
| """ | ||
| Verify the cryptographic integrity of a report using the blockchain-style chaining. | ||
| Optimized: Uses column projection to fetch only needed data. | ||
| Optimized: Uses stored previous_integrity_hash for O(1) chain verification. | ||
| """ | ||
| # Fetch current issue data | ||
| # Fetch current issue data (Performance Boost: Fetch only needed columns) | ||
| current_issue = await run_in_threadpool( | ||
| lambda: db.query( | ||
| Issue.id, Issue.description, Issue.category, Issue.integrity_hash | ||
| Issue.id, | ||
| Issue.reference_id, | ||
| Issue.description, | ||
| Issue.category, | ||
| Issue.latitude, | ||
| Issue.longitude, | ||
| Issue.user_email, | ||
| Issue.integrity_hash, | ||
| Issue.previous_integrity_hash | ||
| ).filter(Issue.id == issue_id).first() | ||
| ) | ||
|
|
||
| if not current_issue: | ||
| raise HTTPException(status_code=404, detail="Issue not found") | ||
|
|
||
| # Fetch previous issue's integrity hash to verify the chain | ||
| prev_issue_hash = await run_in_threadpool( | ||
| lambda: db.query(Issue.integrity_hash).filter(Issue.id < issue_id).order_by(Issue.id.desc()).first() | ||
| ) | ||
|
|
||
| prev_hash = prev_issue_hash[0] if prev_issue_hash and prev_issue_hash[0] else "" | ||
| # Chaining logic depends on when the issue was created (legacy fallback) | ||
| # Optimized: Use stored previous hash if available, otherwise fallback to subquery (O(N) search) | ||
| if current_issue.previous_integrity_hash is not None: | ||
| prev_hash = current_issue.previous_integrity_hash | ||
| # New robust hash formula with fixed float formatting | ||
| lat_str = f"{current_issue.latitude:.7f}" if current_issue.latitude is not None else "None" | ||
| lon_str = f"{current_issue.longitude:.7f}" if current_issue.longitude is not None else "None" | ||
| hash_content = f"{current_issue.reference_id}|{current_issue.description}|{current_issue.category}|{lat_str}|{lon_str}|{current_issue.user_email}|{prev_hash}" | ||
| else: | ||
| # Legacy fallback: Fetch previous issue's hash via subquery | ||
| prev_issue_hash = await run_in_threadpool( | ||
| lambda: db.query(Issue.integrity_hash).filter(Issue.id < issue_id).order_by(Issue.id.desc()).first() | ||
| ) | ||
| prev_hash = prev_issue_hash[0] if prev_issue_hash and prev_issue_hash[0] else "" | ||
| # Legacy hash formula | ||
| hash_content = f"{current_issue.description}|{current_issue.category}|{prev_hash}" | ||
|
|
||
| # Recompute hash based on current data and previous hash | ||
| # Chaining logic: hash(description|category|prev_hash) | ||
| hash_content = f"{current_issue.description}|{current_issue.category}|{prev_hash}" | ||
| computed_hash = hashlib.sha256(hash_content.encode()).hexdigest() | ||
|
|
||
| is_valid = (computed_hash == current_issue.integrity_hash) | ||
|
|
||
| if is_valid: | ||
|
|
@@ -702,3 +724,43 @@ def get_recent_issues( | |
| # Thread-safe cache update | ||
| recent_issues_cache.set(data, cache_key) | ||
| return data | ||
|
|
||
| @router.get("/api/issues/{issue_id}", response_model=IssueResponse) | ||
| async def get_issue_by_id(issue_id: int, db: Session = Depends(get_db)): | ||
| """ | ||
| Get a single issue by its ID. | ||
| Optimized: Uses column projection for efficient retrieval. | ||
| """ | ||
| # Performance Boost: Use column projection instead of loading full model | ||
| issue = await run_in_threadpool( | ||
| lambda: db.query( | ||
| Issue.id, | ||
| Issue.category, | ||
| Issue.description, | ||
| Issue.created_at, | ||
| Issue.image_path, | ||
| Issue.status, | ||
| Issue.upvotes, | ||
| Issue.location, | ||
| Issue.latitude, | ||
| Issue.longitude, | ||
| Issue.action_plan | ||
| ).filter(Issue.id == issue_id).first() | ||
| ) | ||
|
|
||
| if not issue: | ||
| raise HTTPException(status_code=404, detail="Issue not found") | ||
|
|
||
| return { | ||
| "id": issue.id, | ||
| "category": issue.category, | ||
| "description": issue.description, | ||
| "created_at": issue.created_at, | ||
| "image_path": issue.image_path, | ||
| "status": issue.status, | ||
| "upvotes": issue.upvotes or 0, | ||
| "location": issue.location, | ||
| "latitude": issue.latitude, | ||
| "longitude": issue.longitude, | ||
| "action_plan": issue.action_plan | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,5 +20,13 @@ export const issuesApi = { | |
|
|
||
| vote: async (id) => { | ||
| return await apiClient.post(`/api/issues/${id}/vote`, {}); // The backend endpoint might not require a body for upvote | ||
| }, | ||
|
|
||
| getById: async (id) => { | ||
| return await apiClient.get(`/api/issues/${id}`); | ||
| }, | ||
|
|
||
| verifyBlockchain: async (id) => { | ||
| return await apiClient.get(`/api/issues/${id}/blockchain-verify`); | ||
| } | ||
|
Comment on lines
+25
to
31
|
||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||
| import React, { useState, useEffect } from 'react'; | ||||||
| import { useParams, useNavigate } from 'react-router-dom'; | ||||||
| import { Camera, Upload, CheckCircle, XCircle, AlertTriangle, ArrowLeft } from 'lucide-react'; | ||||||
| import { Camera, Upload, CheckCircle, XCircle, AlertTriangle, ArrowLeft, ShieldCheck } from 'lucide-react'; | ||||||
| import { issuesApi } from '../api/issues'; | ||||||
|
|
||||||
| const API_URL = import.meta.env.VITE_API_URL || ''; | ||||||
|
|
@@ -14,16 +14,18 @@ const VerifyView = () => { | |||||
| const [image, setImage] = useState(null); | ||||||
| const [result, setResult] = useState(null); | ||||||
| const [error, setError] = useState(null); | ||||||
| const [blockchainLoading, setBlockchainLoading] = useState(false); | ||||||
| const [blockchainResult, setBlockchainResult] = useState(null); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| const fetchIssue = async () => { | ||||||
| try { | ||||||
| const issues = await issuesApi.getRecent(); | ||||||
| const found = issues.find(i => i.id === parseInt(id)); | ||||||
| if (found) { | ||||||
| setIssue(found); | ||||||
| // Optimization: Fetch issue by ID directly (O(1)) instead of searching recent list (O(N)) | ||||||
| const data = await issuesApi.getById(id); | ||||||
| if (data) { | ||||||
| setIssue(data); | ||||||
| } else { | ||||||
| setError("Issue not found in recent list."); | ||||||
| setError("Issue not found."); | ||||||
| } | ||||||
| } catch (err) { | ||||||
| console.error("Load failed", err); | ||||||
|
|
@@ -42,6 +44,23 @@ const VerifyView = () => { | |||||
| } | ||||||
| }; | ||||||
|
|
||||||
| const handleBlockchainVerify = async () => { | ||||||
| setBlockchainLoading(true); | ||||||
| setBlockchainResult(null); | ||||||
| try { | ||||||
| const data = await issuesApi.verifyBlockchain(id); | ||||||
| setBlockchainResult(data); | ||||||
| } catch (err) { | ||||||
| console.error("Blockchain verification failed", err); | ||||||
| setBlockchainResult({ | ||||||
| is_valid: false, | ||||||
| message: "Failed to connect to the blockchain verification service." | ||||||
| }); | ||||||
| } finally { | ||||||
| setBlockchainLoading(false); | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| const handleVerify = async () => { | ||||||
| if (!image) return; | ||||||
| setVerifying(true); | ||||||
|
|
@@ -141,6 +160,48 @@ const VerifyView = () => { | |||||
| )} | ||||||
| </div> | ||||||
| </div> | ||||||
|
|
||||||
| {/* Blockchain Integrity Seal Section */} | ||||||
| <div className="bg-gradient-to-br from-indigo-50 to-blue-50 rounded-xl shadow-sm border border-indigo-100 p-6 mb-6"> | ||||||
| <div className="flex items-center gap-3 mb-4"> | ||||||
| <ShieldCheck className="text-indigo-600" size={28} /> | ||||||
| <h2 className="text-xl font-bold text-indigo-900">Blockchain Integrity Seal</h2> | ||||||
| </div> | ||||||
|
|
||||||
| <p className="text-indigo-800 text-sm mb-4"> | ||||||
| Every report in our system is cryptographically sealed and linked to the previous report, creating an immutable chain of records. Verify that this report hasn't been tampered with. | ||||||
|
||||||
| Every report in our system is cryptographically sealed and linked to the previous report, creating an immutable chain of records. Verify that this report hasn't been tampered with. | |
| Reports in our system can be cryptographically sealed and linked together to create an immutable chain of records. When a cryptographic seal is available, you can verify that this report hasn't been tampered with. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
passlib bcrypt compatibility issues breaking changes 2024 2025💡 Result:
Key Passlib ↔ bcrypt compatibility / breaking-change issues (2024–2025)
bcrypt 4.1.0was yanked explicitly due to “Incompatibility with assumptions made by passlib”. [1]bcrypt 4.1.1notes it fixed incompatibility with passlib package detection assumptions. [2]AttributeError: module 'bcrypt' has no attribute '__about__'). [3]bcrypt.hashpw()with a password longer than 72 bytes now raisesValueError; previously it was silently truncated. This can break applications/tests that relied on truncation (and some Passlib code paths). [2]setuptoolsmay not be present by default), “old passlib” can fail to import unlesssetuptoolsis added (leading some projects to switch to forks likelibpass). [4]Practical mitigations people used
__about__/version-reading issue) until upgrading off Passlib or to a maintained fork. [3]bcrypt 5.0.0+, ensure your code never passes >72 bytes to bcrypt (e.g., pre-hash with SHA-256/Blake2 then base64) to avoidValueError. [2]Sources: bcrypt PyPI release history (yanked 4.1.0) [1], bcrypt 5.0.0 changelog (includes 4.1.1 note + 72-byte
ValueError) [2], example failure report showing__about__error surfaced via Passlib [3], Flask-Security note on Python 3.12 / passlib / setuptools andlibpass[4].🏁 Script executed:
# Check the full requirements-render.txt file cat -n backend/requirements-render.txtRepository: RohanExploit/VishwaGuru
Length of output: 429
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 123
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 385
🏁 Script executed:
# Check the other requirements file for comparison cat -n backend/requirements.txtRepository: RohanExploit/VishwaGuru
Length of output: 684
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 434
🏁 Script executed:
Repository: RohanExploit/VishwaGuru
Length of output: 49
🌐 Web query:
python-jose cryptography passlib bcrypt current versions 2025💡 Result:
Latest PyPI releases during 2025 (i.e., “current” by end of 2025):
Sources: [1] [2] [3] [4]
Unpinned security-sensitive dependencies and inconsistent dependency specification risk breaking production builds.
The four packages lack version pins and are split into separate entries, unlike the main
requirements.txtwhich uses extras syntax (python-jose[cryptography],passlib[bcrypt]). Splitting these into separate entries means pip doesn't enforce the link betweenpassliband itsbcryptbackend — reducing robustness.Additionally, uncontrolled versions are risky for production.
bcrypt 5.0.0(Sep 2025) introduced a breaking change:hashpw()now raisesValueErrorfor passwords longer than 72 bytes (previously silently truncated). An unpinned deployment could pull bcrypt 5.0.0 and break authentication if the application or passlib passes untruncated passwords.Consider reverting to the extras syntax from
requirements.txtand adding version pins to guard against breaking changes:Suggested pins
The
bcrypt>=4.1.1,<5pin avoids both the yanked 4.1.0 (which broke passlib version detection) and the 5.0.0 breaking change with long passwords.📝 Committable suggestion
🤖 Prompt for AI Agents