-
Notifications
You must be signed in to change notification settings - Fork 36
⚡ Bolt: Optimized blockchain integrity system with last-hash caching #501
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
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,13 +172,18 @@ 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 "" | ||||||||||||||||||||||||||
| # Optimization: Use cache for the last hash to maintain the chain with near-zero overhead | ||||||||||||||||||||||||||
| prev_hash = blockchain_last_hash_cache.get("last_integrity_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() | ||||||||||||||||||||||||||
|
Comment on lines
+179
to
+181
|
||||||||||||||||||||||||||
| # 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() | |
| # Cache miss: Fetch only the last *non-null* hash from DB | |
| prev_issue = await run_in_threadpool( | |
| lambda: db.query(Issue.integrity_hash) | |
| .filter(Issue.integrity_hash.isnot(None)) | |
| .order_by(Issue.id.desc()) | |
| .first() |
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.
Chain fork risk under concurrent issue creation (TOCTOU on prev_hash).
Line 176 reads cached parent hash and Line 215 updates cache only after persistence. Concurrent requests can compute different new blocks from the same parent, breaking linear chain guarantees.
🔧 Suggested fix (serialize chain head assignment in app process)
+import asyncio
+
+# Serialize blockchain head updates inside this worker process
+_blockchain_chain_lock = asyncio.Lock()
...
- prev_hash = blockchain_last_hash_cache.get("last_integrity_hash")
-
- if prev_hash is None:
- 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_integrity_hash")
-
- hash_content = f"{description}|{category}|{prev_hash}"
- integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest()
+ async with _blockchain_chain_lock:
+ prev_hash = blockchain_last_hash_cache.get("last_integrity_hash")
+ if prev_hash is None:
+ 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_integrity_hash")
+
+ hash_content = f"{description}|{category}|{prev_hash}"
+ integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest()
new_issue = Issue(
...
integrity_hash=integrity_hash,
previous_integrity_hash=prev_hash
)
- await run_in_threadpool(save_issue_db, db, new_issue)
-
- blockchain_last_hash_cache.set(data=integrity_hash, key="last_integrity_hash")
+ await run_in_threadpool(save_issue_db, db, new_issue)
+ blockchain_last_hash_cache.set(data=integrity_hash, key="last_integrity_hash")For multi-worker deployments, use a DB-backed lock/chain-head row so serialization is global, not per process.
Also applies to: 214-215
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/routers/issues.py` around lines 176 - 185, The current code reads
prev_hash from blockchain_last_hash_cache and then computes/creates a new block
without global serialization, causing TOCTOU forks; replace this
local-cache-only pattern by acquiring a DB-backed chain-head lock/row when
generating a new issue: within a DB transaction select the chain-head row (or a
dedicated single-row table) FOR UPDATE to read the authoritative parent hash
(instead of the cache), compute and persist the new Issue (using
Issue.integrity_hash logic), update the chain-head row to point to the new hash,
commit, and only then update blockchain_last_hash_cache with the committed
value; ensure the code paths referencing prev_hash, blockchain_last_hash_cache,
and the run_in_threadpool/Issue.integrity_hash query use this transactional lock
to serialize head assignment across processes.
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.
P2: Integrity verification is weakened by trusting previous_integrity_hash directly instead of validating against the canonical previous issue hash.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/issues.py, line 646:
<comment>Integrity verification is weakened by trusting `previous_integrity_hash` directly instead of validating against the canonical previous issue hash.</comment>
<file context>
@@ -620,24 +629,28 @@ def get_user_issues(
-
- prev_hash = prev_issue_hash[0] if prev_issue_hash and prev_issue_hash[0] else ""
+ # Use stored previous hash if available, otherwise fall back to DB lookup (for legacy records)
+ if current_issue.previous_integrity_hash is not None:
+ prev_hash = current_issue.previous_integrity_hash
+ else:
</file context>
| if current_issue.previous_integrity_hash is not None: | |
| prev_hash = current_issue.previous_integrity_hash | |
| else: | |
| # Fallback for legacy records without previous_integrity_hash column populated | |
| 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 "" | |
| 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 "" |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -29,7 +29,8 @@ def test_blockchain_verification_success(client, db_session): | |||||
| issue1 = Issue( | ||||||
| description="First issue", | ||||||
| category="Road", | ||||||
| integrity_hash=hash1 | ||||||
| integrity_hash=hash1, | ||||||
| previous_integrity_hash="" | ||||||
| ) | ||||||
| db_session.add(issue1) | ||||||
| db_session.commit() | ||||||
|
|
@@ -42,7 +43,8 @@ def test_blockchain_verification_success(client, db_session): | |||||
| issue2 = Issue( | ||||||
| description="Second issue", | ||||||
| category="Garbage", | ||||||
| integrity_hash=hash2 | ||||||
| integrity_hash=hash2, | ||||||
| previous_integrity_hash=hash1 | ||||||
| ) | ||||||
| db_session.add(issue2) | ||||||
| db_session.commit() | ||||||
|
|
@@ -62,6 +64,41 @@ def test_blockchain_verification_success(client, db_session): | |||||
| assert data["is_valid"] == True | ||||||
| assert data["current_hash"] == hash2 | ||||||
|
|
||||||
| def test_blockchain_previous_hash_stored(client, db_session): | ||||||
| # Create first issue | ||||||
| hash1 = hashlib.sha256(b"First").hexdigest() | ||||||
| issue1 = Issue(description="First", category="Road", integrity_hash=hash1) | ||||||
| db_session.add(issue1) | ||||||
| db_session.commit() | ||||||
|
|
||||||
| # Create second issue via API (to test logic in create_issue) | ||||||
| # Mocking background tasks to avoid errors | ||||||
| from unittest.mock import patch | ||||||
| with patch("backend.routers.issues.process_action_plan_background"), \ | ||||||
| patch("backend.routers.issues.create_grievance_from_issue_background"): | ||||||
| response = client.post( | ||||||
| "/api/issues", | ||||||
| data={ | ||||||
| "description": "Second issue description", | ||||||
| "category": "Garbage", | ||||||
| "latitude": 10.0, | ||||||
| "longitude": 20.0 | ||||||
| } | ||||||
| ) | ||||||
|
|
||||||
| assert response.status_code == 201 | ||||||
| issue_id = response.json()["id"] | ||||||
|
|
||||||
| # Verify issue in DB has previous_integrity_hash | ||||||
| issue2 = db_session.query(Issue).filter(Issue.id == issue_id).first() | ||||||
| assert issue2.previous_integrity_hash == hash1 | ||||||
|
|
||||||
|
Comment on lines
+67
to
+95
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. Reset blockchain cache in test setup to avoid order-dependent failures. This test assumes cache state is empty, but ✅ Suggested test hardening from backend.models import Issue
+from backend.cache import blockchain_last_hash_cache
...
def test_blockchain_previous_hash_stored(client, db_session):
+ blockchain_last_hash_cache.invalidate("last_integrity_hash")
+
# Create first issue
hash1 = hashlib.sha256(b"First").hexdigest()🤖 Prompt for AI Agents
Comment on lines
+67
to
+95
|
||||||
| # Verify blockchain-verify endpoint uses the stored hash | ||||||
| # (Implicitly tested if it returns is_valid=True and we know we didn't mock the internal DB query in the router) | ||||||
| response = client.get(f"/api/issues/{issue_id}/blockchain-verify") | ||||||
| assert response.status_code == 200 | ||||||
| assert response.json()["is_valid"] == True | ||||||
|
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. Use direct truth assertion instead of Prefer a direct truthy check on Line 100 for clarity and lint compliance. ✏️ Suggested fix- assert response.json()["is_valid"] == True
+ assert response.json()["is_valid"]📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.2)[error] 100-100: Avoid equality comparisons to Replace with (E712) 🤖 Prompt for AI Agents |
||||||
|
|
||||||
| def test_blockchain_verification_failure(client, db_session): | ||||||
| # Create issue with tampered hash | ||||||
| issue = Issue( | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.
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.
P1: Using a process-local last-hash cache to build
previous_integrity_hashcan create incorrect parent links (forked chain) across workers/processes.Prompt for AI agents