-
Notifications
You must be signed in to change notification settings - Fork 35
⚡ Bolt: Optimized Blockchain Integrity and Admin Stats #516
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -30,7 +30,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| send_status_notification | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.spatial_utils import get_bounding_box, find_nearby_issues | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.cache import recent_issues_cache, nearby_issues_cache | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.cache import recent_issues_cache, nearby_issues_cache, blockchain_last_hash_cache | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.hf_api_service import verify_resolution_vqa | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.dependencies import get_http_client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.rag_service import rag_service | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -172,16 +172,23 @@ async def create_issue( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev_issue = await run_in_threadpool( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lambda: db.query(Issue.integrity_hash).order_by(Issue.id.desc()).first() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev_hash = prev_issue[0] if prev_issue and prev_issue[0] else "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Performance Boost: Use thread-safe cache to eliminate DB query for last hash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev_hash = blockchain_last_hash_cache.get("last_hash") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if prev_hash is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Cache miss: Fetch only the last hash from DB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev_issue = await run_in_threadpool( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lambda: db.query(Issue.integrity_hash).order_by(Issue.id.desc()).first() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev_hash = prev_issue[0] if prev_issue and prev_issue[0] else "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| blockchain_last_hash_cache.set(data=prev_hash, key="last_hash") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Simple but effective SHA-256 chaining | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Simple but effective SHA-256 chaining | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hash_content = f"{description}|{category}|{prev_hash}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Update cache for next report | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| blockchain_last_hash_cache.set(data=integrity_hash, key="last_hash") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Do not update Prompt for AI agents
Comment on lines
+175
to
+190
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Race condition breaks blockchain chain integrity. The read-compute-write sequence for Scenario:
Both issues now have 🔒 Recommended fix: Use database-level sequencingThe simplest reliable fix is to query the previous hash from the database within the same transaction that inserts the new issue, leveraging DB-level locking: 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
- # Performance Boost: Use thread-safe cache to eliminate DB query for last hash
- prev_hash = blockchain_last_hash_cache.get("last_hash")
- if prev_hash is None:
- # Cache miss: Fetch only the last hash from DB
- prev_issue = await run_in_threadpool(
- lambda: db.query(Issue.integrity_hash).order_by(Issue.id.desc()).first()
- )
- prev_hash = prev_issue[0] if prev_issue and prev_issue[0] else ""
- blockchain_last_hash_cache.set(data=prev_hash, key="last_hash")
-
- # Simple but effective SHA-256 chaining
- hash_content = f"{description}|{category}|{prev_hash}"
- integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest()
-
- # Update cache for next report
- blockchain_last_hash_cache.set(data=integrity_hash, key="last_hash")
+ # Blockchain feature: calculate integrity hash for the report
+ # Query previous hash within the same transaction for consistency
+ prev_issue = await run_in_threadpool(
+ lambda: db.query(Issue.integrity_hash).order_by(Issue.id.desc()).with_for_update().first()
+ )
+ prev_hash = prev_issue[0] if prev_issue and prev_issue[0] else ""
+
+ # Simple but effective SHA-256 chaining
+ hash_content = f"{description}|{category}|{prev_hash}"
+ integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest()Using 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+189
to
+191
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # RAG Retrieval (New) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| relevant_rule = rag_service.retrieve(description) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| initial_action_plan = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -199,7 +206,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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -620,24 +628,32 @@ 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 previous_integrity_hash column for O(1) verification. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fetch current issue data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fetch current issue data including the link to previous hash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Performance Boost: Use projected previous_integrity_hash to avoid N+1 or secondary lookups | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| current_issue = await run_in_threadpool( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lambda: db.query( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Issue.id, Issue.description, Issue.category, Issue.integrity_hash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Issue.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Issue.description, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Issue.category, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Determine previous hash (use stored link or fallback for legacy records) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev_hash = current_issue.previous_integrity_hash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev_hash = prev_issue_hash[0] if prev_issue_hash and prev_issue_hash[0] else "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if prev_hash is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fallback for legacy records created before O(1) optimization | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+648
to
+652
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Recompute hash based on current data and previous hash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Chaining logic: hash(description|category|prev_hash) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
The get→compute→set sequence for "last_hash" isn’t atomic. Under concurrent create_issue requests, multiple workers/threads can read the same prev_hash and produce a forked chain (and with multiple Uvicorn/Gunicorn workers, each process has its own cache). Consider serializing hash generation with a lock or deriving prev_hash from the database within the same transaction to preserve a single chain.
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.
@copilot open a new pull request to apply changes based on this feedback
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.
I've implemented atomic hash calculation and record persistence by using a single database transaction with
with_for_update()on the tail record. This serializes concurrent creations and prevents chain forks, as requested.