Backend internals¶
The backend is a Django 6 project (backend/blindproof_backend/) with the bulk of the logic in a single app (backend/api/). This page documents the models, storage abstraction, Merkle aggregation, OpenTimestamps integration, and proof-bundle assembly. See Backend API for the endpoint surface.
Data model¶
All in backend/api/models.py.
User¶
Custom Django user with email as USERNAME_FIELD.
| Field | Type | Notes |
|---|---|---|
email |
EmailField(unique) |
Username. |
argon2_salt |
BinaryField |
16 bytes, generated at enrolment. Sent to the client so it can derive the master key. |
created_at |
DateTimeField |
|
attestation_signed_at |
DateTimeField, null |
Set on first POST /proof-bundle. |
attestation_hash |
BinaryField(64), null |
SHA-512 of the canonical attestation text — lets us reject in-place edits later. |
is_active |
BooleanField |
AuthToken¶
Opaque bearer tokens (64-char unique hex string). Tokens are long-lived; there's no automatic expiry in the POC. A user may have many — one per device, broadly.
Snapshot¶
The core event — one row per successfully uploaded save.
| Field | Type | Notes |
|---|---|---|
user |
FK(User) |
|
captured_at |
DateTimeField |
Client's wall clock at the save. |
file_type |
CharField(16) |
E.g. "md", "txt". |
path_ciphertext |
BinaryField |
AES-GCM ciphertext of the file path. |
path_nonce |
BinaryField |
12 bytes, fresh per upload (not per capture). |
plaintext_hmac |
BinaryField |
HMAC-SHA256(mac_key, plaintext). Used as the Merkle leaf. |
ciphertext_ref |
CharField(64, unique) |
UUID-v4 hex; doubles as the blob-storage key. |
ciphertext_size |
IntegerField |
After encryption. |
ciphertext_nonce |
BinaryField |
12 bytes, per capture. |
word_count, char_count |
IntegerField |
Computed client-side, sent in plaintext (metadata). |
source_timestamp |
DateTimeField, null |
For connectors that surface an external timestamp (e.g. Google Docs revisions). |
uploaded_at |
DateTimeField(auto_now_add) |
Server stamp. |
merkle_root |
FK(MerkleRoot, null) |
Populated by aggregate_day. |
MerkleRoot¶
| Field | Type | Notes |
|---|---|---|
user |
FK(User) |
Each author has their own tree — daily roots don't mix users. |
root_hash |
BinaryField |
32 bytes. The public anchor. |
computed_at |
DateTimeField |
End of the UTC day the root covers. |
OTSProof¶
| Field | Type | Notes |
|---|---|---|
merkle_root |
OneToOneField(MerkleRoot) |
|
receipt_bytes |
BinaryField |
Serialized opentimestamps DetachedTimestampFile. |
submitted_at |
DateTimeField |
When submit_ots_receipts last touched it. |
bitcoin_block_height |
IntegerField, null |
Set by upgrade_ots_receipts once a calendar returns a Bitcoin attestation. null means "still pending". |
Blob storage¶
A BlobStorage Protocol (backend/api/storage.py) lets the storage backend slot behind the API. The POC ships LocalBlobStorage(root) writing <root>/<ciphertext_ref>.bin; an S3/B2 implementation is trivial to add (deferred until volume storage becomes a bottleneck). Configured via the BLOB_STORAGE_ROOT setting.
Merkle aggregation¶
backend/api/merkle.py:
build_merkle(leaves: list[bytes]) -> bytes— pure SHA-256 binary tree. Pairs are hashed assha256(left + right). On odd levels the last node is duplicated (Bitcoin-style). A single-leaf tree returns the leaf unchanged.aggregate_day(user, day)— wraps all ofuser's snapshots captured in the given UTC day, builds the Merkle root, writes aMerkleRootrow, and links eachSnapshot.merkle_rootin one transaction.aggregate_dayalso exists as a Django management command (manage.py aggregate_day --yesterday), which is what the daily workflow calls.
The OTS lifecycle¶
flowchart TD
A["Yesterday's HMAC commitments<br/>across all snapshots"] --> B[build_merkle]
B --> C[Merkle root stored]
C --> D["submit_ots_receipts<br/>→ 3 public calendars"]
D --> E["Pending receipt<br/>OTSProof.receipt_bytes"]
E -->|2-6 hours later| F["Bitcoin confirms<br/>calendar timestamp"]
F --> G["upgrade_ots_receipts<br/>merges Bitcoin attestation"]
G --> H["OTSProof.bitcoin_block_height set<br/>verify.py now returns PASS"]
All of this lives in backend/api/ots.py:
OTSSubmitter— a Protocol definingsubmit(digest) -> bytesandupgrade(receipt_bytes) -> (bytes, int | None).FakeOTSSubmitter— deterministic, for tests and local development. Produces a receipt with a recognisableFAKE-OTS-RECEIPT-FOR-…prefix thatverify.pydetects and explicitly refuses to claim as anchored.OpenTimestampsSubmitter— the real submitter. Nonces the digest, fans out to the three default public calendars (alice.btc,bob.btc,finney) viaopentimestamps.calendar.RemoteCalendar, tolerates partial success, returns a serialisedDetachedTimestampFile.get_ots_submitter()— factory that readssettings.OTS_SUBMITTER(sourced from theBLIND_OTS_MODEenv var; defaults tofakeso no mis-configured environment can accidentally spam the public calendars).
Management commands:
submit_ots_receipts— walksMerkleRoots without anOTSProofand hands each to the configured submitter.upgrade_ots_receipts— walksOTSProofs without abitcoin_block_height, hits each pending attestation's calendar viaget_timestamp(commitment), and if the calendar now has a Bitcoin attestation, merges it and stamps the block height.
The daily GitHub Actions workflow runs upgrade_ots_receipts → aggregate_day (yesterday) → submit_ots_receipts in that order. See Deployment & CI.
Proof bundle assembly¶
backend/api/bundle_builder.py is the orchestrator. It pulls:
assemble_bundle_data(user)— gathers the user's snapshots, merkle roots, OTS receipts, and writing stats into a single data structure.canonical_bundle_json(data)inbackend/api/bundle.py— produces a deterministic, sorted-keys UTF-8 JSON payload. Content-addressed viabundle_identifier_hex(SHA-256 of the canonical bytes).sign_canonical_bytes(bytes, key)inbackend/api/signing.py— Ed25519 signature viacryptography. Producesbundle.sig.json.render_bundle_pdf(data)inbackend/api/pdf.py— reportlab, native Drawing primitives for the timeline chart. No PNG intermediate, no extra system libraries required.verify_template.py— the stdlib-only verifier, shipped inside the zip asverify.py.
build_bundle_zip(user) bundles all four into a single zip ready to hand to the author.
The Ed25519 signing key lives as the BLINDPROOF_SIGNING_KEY Fly secret in production. Its public-key fingerprint is embedded in every bundle.sig.json so verify.py can confirm the bundle came from our signing key and was not altered after signing. Key rotation in V1; until then, the fingerprint is expected to be stable.
See Proof bundle format for the exact on-wire format of each of the four files.
Settings of note¶
All in backend/blindproof_backend/settings.py. Env-driven in production, defaulted for dev:
| Setting | Env var | Purpose |
|---|---|---|
SECRET_KEY |
DJANGO_SECRET_KEY |
Django. |
DEBUG |
DJANGO_DEBUG |
|
ALLOWED_HOSTS |
DJANGO_ALLOWED_HOSTS |
|
| DB path | DJANGO_DB_PATH |
SQLite on the Fly volume in production. |
BLOB_STORAGE_ROOT |
BLIND_BLOB_ROOT |
Local-FS blob store root. |
| OTS mode | BLIND_OTS_MODE |
fake or real. |
| Signing key | BLINDPROOF_SIGNING_KEY |
Ed25519 secret key (hex). |
Postgres migration is deferred — SQLite on a Fly volume is adequate for the POC traffic level.