Skip to content

Backend blockchain#17

Merged
Aoibheannmangan merged 7 commits into
mainfrom
backend-blockchain
May 24, 2026
Merged

Backend blockchain#17
Aoibheannmangan merged 7 commits into
mainfrom
backend-blockchain

Conversation

@sarahmc253
Copy link
Copy Markdown
Owner

@sarahmc253 sarahmc253 commented May 24, 2026

this nearly killed me

Summary by CodeRabbit

  • New Features

    • Logout now attempts server-side sign-out and always clears local session
    • On-chain message anchoring with automatic background processing and a manual flush endpoint
  • Bug Fixes

    • Registration/login enforce updated client crypto payload fields and return consistent errors
    • Sending messages now validates content hashes and returns created message IDs
  • Documentation

    • Extended session transcript with design decisions and implementation notes
  • Maintenance

    • Background scheduler added to run anchoring and confirmation jobs periodically

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements two-phase on-chain anchoring (submit + confirm) with scheduler jobs, renames client/server E2E crypto fields, validates and persists message content_hashes, adds logout/flush anchoring triggers, and records design decisions in the transcript.

Changes

Blockchain Message Anchoring with Crypto Field Rename

Layer / File(s) Summary
Crypto field rename and auth schema alignment
client-web/js/api.js, server/app/auth/routes.py
Client and server adopt consistent E2E crypto field names: wrapped_private_key and kek_salt replace prior HPKE/Argon2-specific names. Auth registration derives/stores password_salt and persists password_hash/password_salt and renamed key fields; login selects and returns the renamed fields. A JWT-protected /logout route may spawn anchoring work.
Message send, validation, and manual flush
server/app/messages/routes.py
POST /messages enforces required string fields, validates and normalizes content_hash to 0x-prefixed 64-hex, generates UUID/UTC timestamp, inserts with commit/rollback and maps FK errors to 404. Authenticated /flush spawns a daemon thread to run user-scoped anchoring (202 or 503 if disabled).
Anchoring core: Merkle root, reservation, and tx submission
server/app/messages/anchor.py
anchor_pending() groups unanchored messages by conversation, computes Merkle roots, inserts blockchain_records, reserves anchoring by updating messages.blockchain_record_id before sending storeData(root) via Web3, manages per-row commits/rollbacks, rolls back on chain-send failure, and updates blockchain_records.tx_hash on success. confirm_pending() backfills block_number/block_timestamp from receipts/blocks and releases failed reservations.
App factory scheduler and config validation
requirements.txt, server/app/__init__.py
Adds apscheduler>=3.11.0, validates WEB3_RPC_URL, CONTRACT_ADDRESS, WALLET_PRIVATE_KEY in create_app(), sets ANCHORING_ENABLED, and starts a BackgroundScheduler (5-minute anchor job, 2-minute confirm job) with a debug/reloader guard; scheduler stored on app.scheduler.
Design documentation and decision record
docs/transcript_sarah.md
Transcript expanded with anchoring tradeoffs (per-message vs batching, client vs server Merkle approaches), flushing strategies, schema rename notes, use of current_app.config, blockchain_records mapping, and two-phase anchoring design.

Sequence Diagram

sequenceDiagram
  participant Client as Client
  participant ClientAPI as client-web/api.js
  participant Server as FlaskApp
  participant DB as MySQL
  participant Web3 as Blockchain
  Client->>ClientAPI: POST /messages
  ClientAPI->>Server: /messages handler (validated JSON)
  Server->>DB: INSERT message row
  Client->>ClientAPI: logout() -> POST /auth/logout
  ClientAPI->>Server: POST /auth/logout (Bearer)
  Server->>Server: spawn anchor_pending thread
  Server->>DB: SELECT unanchored messages
  Server->>DB: INSERT blockchain_records
  Server->>DB: UPDATE messages.blockchain_record_id
  Server->>Web3: sendTransaction storeData(merkle_root)
  alt tx success
    Web3->>Server: tx_hash
    Server->>DB: UPDATE blockchain_records.tx_hash
  else tx pending or failure
    Server->>DB: rollback reservation or increment revert_count
  end
  Server->>Web3: confirm_pending fetch receipts
  Web3->>Server: receipt
  Server->>DB: UPDATE blockchain_records block_number/timestamp
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • sarahmc253/WhatSaS#7: Overlaps on auth field renaming and crypto payload handling in registration/login flows.
  • sarahmc253/WhatSaS#6: Related changes to app factory and route scaffolding used by this PR.

Suggested reviewers

  • sreejita-saha
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Backend blockchain" is vague and overly broad; it fails to convey the specific nature of the changes, such as blockchain anchoring for messages, Argon2 password hashing refactoring, or the crypto-material field renaming. Consider a more descriptive title that captures the main changes, such as "Add blockchain anchoring for messages with crypto-field renaming and Argon2 hashing" or similar.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backend-blockchain

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/app/__init__.py`:
- Around line 71-89: The code starts BackgroundScheduler inside create_app()
causing one scheduler per worker (functions
_scheduled_anchor/_scheduled_confirm, app.scheduler) — replace this by ensuring
a single-owner process: either move scheduler startup out of web workers into a
dedicated service/process that imports anchor_pending/confirm_pending, or
implement a production-only single-owner gate (e.g., an explicit env flag like
SCHEDULER_OWNER set on one instance, or a distributed leader-lock using Redis/DB
before starting the scheduler) so only the owner calls scheduler.start(); remove
reliance on WERKZEUG_RUN_MAIN for production.

In `@server/app/auth/routes.py`:
- Around line 162-178: The logout flow has two problems: the background thread
raised NameError because anchor_pending is imported only inside logout() but
used by _anchor_in_context(), and the web client never calls POST /auth/logout
so anchoring never triggers; fix by moving the import of anchor_pending to
module scope (import anchor_pending at top of server/app/auth/routes.py) so
_anchor_in_context and the thread can access it, and update the client API
logout to call POST /auth/logout before clearing sessionStorage (make
api.logout() async and await a POST request to '/auth/logout' then clearToken in
finally), and update any callers of api.logout() to handle the returned promise
to avoid unhandled rejections.

In `@server/app/messages/anchor.py`:
- Around line 66-156: The _run worker currently reads messages with
blockchain_record_id IS NULL then sends the on-chain tx and only afterwards
writes blockchain_records and updates messages, which can cause duplicate
anchors or orphaned tx; modify _run to atomically claim messages before
submitting the external transaction by starting a DB transaction and either (a)
selecting message rows with SELECT ... FOR UPDATE SKIP LOCKED (using the same
cursor and transaction) and setting a temporary claim flag/anchoring state on
messages, or (b) INSERT a blockchain_records row with id/status='pending' and
update messages.blockchain_record_id to that id and COMMIT before calling
contract.functions.storeData, then proceed with the on-chain send and finally
update the blockchain_records row with tx_hash/block_number/block_timestamp and
clear/advance the anchoring state; ensure you use the same unique symbols
(messages table, blockchain_records table, blockchain_record_id column, the _run
function and storeData contract call) and handle DB rollbacks and nonce/gas
error paths so claims are durable and retries are idempotent.
- Around line 196-225: The loop currently treats any fetched transaction receipt
as confirmed; modify the logic after fetching receipt in
server/app/messages/anchor.py to check receipt['status'] (from
w3.eth.get_transaction_receipt) and only proceed to fetch block, update
blockchain_records (the cursor.execute call that sets block_number and
block_timestamp) and call logger.info('Confirmed tx %s at block %s', ...) when
receipt['status'] == 1; if status == 0 (reverted) skip the DB update and instead
log (e.g., logger.warning or logger.info) that the tx reverted and continue to
next row so reverted transactions are not recorded as confirmed. Ensure you
reference the same variables (receipt, block, block_number, row['id'],
row['tx_hash']) and keep existing exception handling around
get_transaction_receipt and get_block.

In `@server/app/messages/routes.py`:
- Around line 80-89: In flush(), before spawning the background Thread, check
the application config flag that controls anchoring (e.g.
app.config.get('ANCHORING_ENABLED') or the Web3 config presence) using the app
object obtained via current_app._get_current_object(); if anchoring is disabled,
return an HTTP error (e.g. 403) and do not start the thread or call
_anchor_in_context, otherwise proceed to spawn the daemon thread as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fbab3b60-0b49-4d71-bb10-385f80b381d0

📥 Commits

Reviewing files that changed from the base of the PR and between a404348 and ae59d8d.

📒 Files selected for processing (7)
  • client-web/js/api.js
  • docs/transcript_sarah.md
  • requirements.txt
  • server/app/__init__.py
  • server/app/auth/routes.py
  • server/app/messages/anchor.py
  • server/app/messages/routes.py

Comment thread server/app/__init__.py
Comment thread server/app/auth/routes.py
Comment thread server/app/messages/anchor.py
Comment thread server/app/messages/anchor.py
Comment thread server/app/messages/routes.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/app/messages/anchor.py (1)

116-156: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Seed anchoring run nonce from the pending pool
w3.eth.get_transaction_count(account.address) defaults to block_identifier=latest (via w3.eth.default_block), which excludes pending transactions; this can cause nonce reuse/replacement while prior anchor txs are still pending. Use 'pending' for nonce management.

Suggested fix
-    nonce = w3.eth.get_transaction_count(account.address)
+    nonce = w3.eth.get_transaction_count(account.address, 'pending')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/messages/anchor.py` around lines 116 - 156, The nonce seeding
currently uses w3.eth.get_transaction_count(account.address) which defaults to
'latest' and ignores pending txs, risking nonce reuse; change the initial nonce
fetch to w3.eth.get_transaction_count(account.address, 'pending') (where nonce
is first assigned) so subsequent
contract.functions.storeData(...)/send_raw_transaction calls use a nonce that
accounts for pending transactions; keep the existing nonce increment (nonce +=
1) behavior unchanged.
♻️ Duplicate comments (1)
server/app/messages/anchor.py (1)

122-168: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Commit the message claim before calling Web3.

This reservation is still not durable enough. messages.blockchain_record_id is updated but left uncommitted until after send_raw_transaction, and the UPDATE ... WHERE id IN (...) can overwrite rows another worker already claimed. If two workers read the same conversation before either commits, both can send a tx and one on-chain spend becomes orphaned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/messages/anchor.py` around lines 122 - 168, The reservation of
messages (INSERT into blockchain_records and the UPDATE messages SET
blockchain_record_id = %s ... using the cursor) must be made durable before
calling Web3; move a db.commit() immediately after the reservation block (after
the INSERT and the UPDATE messages) and before building/signing/sending the
transaction so the claim is committed and other workers won't double-claim. Keep
the existing db.rollback()/logger.exception paths on exceptions and ensure the
subsequent UPDATE of blockchain_records with tx_hash (UPDATE blockchain_records
SET tx_hash = %s WHERE id = %s) still happens after the chain tx and commits as
currently implemented.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/app/messages/anchor.py`:
- Around line 223-225: When receipt['status'] != 1, do not just log and
continue; mark the related blockchain_records row as failed and detach or clear
the blockchain_record_id on linked messages so they can be retried or cleaned
up. Specifically, in the block handling the reverted transaction around the
receipt['status'] check (using row['id'] / row['tx_hash'] and the
blockchain_records table), update that blockchain_records row to a terminal
failed state (e.g., status='failed' or failed_at timestamp) and either set
messages.blockchain_record_id = NULL for messages referencing that
blockchain_record_id or call an existing helper like
mark_reservation_failed(row['id']) / detach_messages_from_record(row['id']) to
rollback the reservation.

---

Outside diff comments:
In `@server/app/messages/anchor.py`:
- Around line 116-156: The nonce seeding currently uses
w3.eth.get_transaction_count(account.address) which defaults to 'latest' and
ignores pending txs, risking nonce reuse; change the initial nonce fetch to
w3.eth.get_transaction_count(account.address, 'pending') (where nonce is first
assigned) so subsequent contract.functions.storeData(...)/send_raw_transaction
calls use a nonce that accounts for pending transactions; keep the existing
nonce increment (nonce += 1) behavior unchanged.

---

Duplicate comments:
In `@server/app/messages/anchor.py`:
- Around line 122-168: The reservation of messages (INSERT into
blockchain_records and the UPDATE messages SET blockchain_record_id = %s ...
using the cursor) must be made durable before calling Web3; move a db.commit()
immediately after the reservation block (after the INSERT and the UPDATE
messages) and before building/signing/sending the transaction so the claim is
committed and other workers won't double-claim. Keep the existing
db.rollback()/logger.exception paths on exceptions and ensure the subsequent
UPDATE of blockchain_records with tx_hash (UPDATE blockchain_records SET tx_hash
= %s WHERE id = %s) still happens after the chain tx and commits as currently
implemented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4108c5a3-ab15-4e7a-9a34-d8c047628d82

📥 Commits

Reviewing files that changed from the base of the PR and between ae59d8d and e5d000e.

📒 Files selected for processing (4)
  • client-web/js/api.js
  • server/app/auth/routes.py
  • server/app/messages/anchor.py
  • server/app/messages/routes.py

Comment thread server/app/messages/anchor.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/app/messages/anchor.py`:
- Around line 223-240: The current revert handler in anchor.py immediately nulls
messages.blockchain_record_id and deletes the blockchain_records row which
requeues the same batch; instead update the existing blockchain_records row to
record the failure (e.g., set status='failed' or increment retry_count and set
next_retry_at/backoff) and leave messages.blockchain_record_id pointing to that
record so anchor_pending() ignores it until retry conditions are met. Modify the
exception branch in the receipt status != 1 block (the code that currently runs
cursor.execute('UPDATE messages SET blockchain_record_id = NULL ...') and DELETE
FROM blockchain_records) to perform an UPDATE on blockchain_records (status,
retry_count, next_retry_at) and avoid nulling messages.blockchain_record_id, and
also ensure anchor_pending() logic is changed to filter out failed records or
defer until next_retry_at. Ensure DB commit/rollback and cursor.close() remain
intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 68a0fe6f-26b5-44df-9915-f50226253740

📥 Commits

Reviewing files that changed from the base of the PR and between e5d000e and c15c554.

📒 Files selected for processing (1)
  • server/app/messages/anchor.py

Comment thread server/app/messages/anchor.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/app/messages/anchor.py`:
- Line 224: Handle potential NULL revert_count by treating None as 0 before
incrementing: replace the direct addition new_count = row['revert_count'] + 1
with code that safely coalesces row['revert_count'] (e.g., current =
row.get('revert_count') or 0 or current = int(row['revert_count'] or 0)) and
then set new_count = current + 1 so that new_count calculation in anchor.py
never raises TypeError when row['revert_count'] is NULL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b79ab263-42f3-40e9-8da9-c9470e3cff4b

📥 Commits

Reviewing files that changed from the base of the PR and between c15c554 and 8fab9c4.

📒 Files selected for processing (1)
  • server/app/messages/anchor.py

Comment thread server/app/messages/anchor.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
server/app/messages/anchor.py (1)

33-42: 💤 Low value

Guard against empty input to avoid IndexError.

If hex_hashes is empty, layer[0] on line 42 raises IndexError. While current callers ensure non-empty input, a defensive check would prevent future misuse.

🛡️ Proposed fix
 def _merkle_root(hex_hashes):
+    if not hex_hashes:
+        raise ValueError("Cannot compute Merkle root of empty list")
     layer = [bytes.fromhex(h.removeprefix('0x')) for h in hex_hashes]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/messages/anchor.py` around lines 33 - 42, The _merkle_root
function can IndexError when hex_hashes is empty; add a defensive check at the
start of _merkle_root to validate hex_hashes is non-empty and raise a clear
ValueError (or another appropriate exception) with a descriptive message (e.g.,
"hex_hashes must be a non-empty iterable") before using layer and accessing
layer[0]; this keeps the existing logic (removeprefix, bytes.fromhex,
Web3.keccak) intact but prevents silent IndexError for future callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/app/messages/anchor.py`:
- Around line 33-42: The _merkle_root function can IndexError when hex_hashes is
empty; add a defensive check at the start of _merkle_root to validate hex_hashes
is non-empty and raise a clear ValueError (or another appropriate exception)
with a descriptive message (e.g., "hex_hashes must be a non-empty iterable")
before using layer and accessing layer[0]; this keeps the existing logic
(removeprefix, bytes.fromhex, Web3.keccak) intact but prevents silent IndexError
for future callers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0273e7be-91c4-4000-abaa-79323cddbfc5

📥 Commits

Reviewing files that changed from the base of the PR and between 8fab9c4 and d0f8d7f.

📒 Files selected for processing (1)
  • server/app/messages/anchor.py

@Aoibheannmangan Aoibheannmangan merged commit 73461ca into main May 24, 2026
1 check passed
@Aoibheannmangan Aoibheannmangan deleted the backend-blockchain branch May 24, 2026 12:48
This was referenced May 24, 2026
This was referenced Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants